Repository navigation
feat(app): 업무판 — the company's task board, dragged by hand, and a room's "알아보는 중" - #268
yulmucoffee wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
🔆 CodeSolar review ✅ Looks good
Review of PR #268 — company hub, task board, and room working line. The diff adds a full company hub service (hub/), company screens in the app, a draggable task board, and a room "알아보는 중" line. I read the visible server-side hub code (auth, events, idempotency, socket, routes, cursor, runtime), the app-side room-working logic, the new-task form, the task card, and the channel event wiring. The server-side hub code is careful and consistent. The app-side code has one real bug and one minor design note.
✅ No issues worth flagging.
⚠️ The diff was large; some files were excluded from the review.
solar-pro4 (openrouter) · 82 files · +16564/-10 · 220.6s · CodeSolar
Comment /codesolar on this PR to re-run the review.
…m's "알아보는 중" 회사 gets a 업무판 part (`/company/tasks`): the tasks this person can see in five columns — 할 일 · 하는 중 · 검토 · 막힘 · 끝 — moved between and within columns by dragging (pointer events, our own code), or from each card's menu for the keyboard; a new task given to nobody, to 내 라피 (`bot:<me>`), to a teammate or to one of this person's routines (`job:<id>`); a task opened shows its notes, a 라피's coded notes (#260) in plain Korean, and the conversation it was worked in when this VM's 라피 worked it. A move carries the version the board read and the rank halfway between its new neighbours, shows at once, and is the hub's to keep: 409 `laf:task_stale` reads the board again and says so; a colleague's move arrives as a `hub.task.changed` frame. The server's doors are their own file (`server/src/hub/routes-ui-tasks.ts`): every call as the person, `as: "member"` and no run id whatever the body says, only the board's fields, a key on every write. In a team room, "@라피" now asks the writer's own 라피 (`mentions: ["lafi"]`), and every member sees "○○의 라피가 알아보는 중" until that 라피 answers — for at most two minutes, since a turn that failed posts nothing to the room; the asker's own screen keeps it while their VM's room run is going, and links to their conversation for the room. Co-authored-by: LAF Agent <274876363+laf-agent@users.noreply.github.com>
Co-authored-by: LAF Agent <274876363+laf-agent@users.noreply.github.com>
… 2608) Co-authored-by: LAF Agent <274876363+laf-agent@users.noreply.github.com>
bce4cb5 to
47cfc03
Compare
|
bugbot run |
There was a problem hiding this comment.
🔆 CodeSolar review 🟡 Minor notes
🔄 2nd review of this pull request · 29 new commit(s) since the last one
Review of PR #268 — 업무판 (company task board) with drag-and-drop, keyboard moves, live updates, and room working line. Found one correctness issue in approvalTopicText and one potential race in drag click-swallowing.
✅ No issues worth flagging.
solar-pro4 (openrouter) · 26 files · +2944/-33 · 185.1s · CodeSolar
Comment /codesolar on this PR to re-run the review.
…ove is on its way A card whose move was still unanswered was held back only from a drag (`canDrag`). Its menu still called `moveTo`, which read the optimistic card and sent a second PATCH with the version the first was about to replace; the hub refused it as `laf:task_stale`, the second place was dropped and the board said somebody else had changed the task. `moveTo` now keeps the cards whose move is on its way in a ref, set the moment the move is sent and cleared when it settles, and sends nothing for one of them — whichever way the move came. The card's menu draws its moves disabled while the card is pending. hub-task-board-pending.test.tsx mounts the board, holds the first PATCH open, presses the menu twice and counts one PATCH; once the hub answers, the next move goes with version 2. With both changes reverted (`git checkout` of task-board.tsx and task-card.tsx) it fails: two PATCHes. Either change alone makes it pass. Co-authored-by: LAF Agent <274876363+laf-agent@users.noreply.github.com>
Co-authored-by: LAF Agent <274876363+laf-agent@users.noreply.github.com>
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 72291bc. Configure here.
There was a problem hiding this comment.
🔆 CodeSolar review ✅ Looks good
🔄 3rd review of this pull request · 2 new commit(s) since the last one
Reviewing PR #268 — 업무판 (company task board) with drag-and-drop, keyboard moves, room working line, and task detail. The PR adds ~3000 lines across app and server, with corresponding tests. Previous review at 47cfc03 was clean; 2 new commits since.
✅ No issues worth flagging.
solar-pro4 (openrouter) · 27 files · +3088/-33 · 254.8s · CodeSolar
Comment /codesolar on this PR to re-run the review.
Stacked on #256 (
laf/hub-ui, 회사, itself on #249 → #247). This branch starts from #256's head68499a8b; until those land the diff againstmainshows their commits too. The task shapes are #253's (laf/hub-tasks), the room "working" rule is #259's (laf/room-answers), and the note codes are the ones #260 (laf/task-pickup) writes — none of those three is merged here; see "For the merge".What this changes
업무판 — the company's task board, in 회사. A fourth part beside 방 · 파일 · 스킬 · 멤버 (
/company/tasks, its own route filecompany.tasks.tsx, drawn bycompany.tsxunder the same title and tabs on the wide measure; 회사 in the top row stays lit on it). Five columns: 할 일 · 하는 중 · 검토 · 막힘 · 끝, each inrankorder.components/tasks/use-board-drag.ts, about 200 lines; no package). A mouse drags the whole card; a finger drags the card's grip (the grip alone istouch-action: none, so a finger elsewhere scrolls the board). The card stays in place, dimmed, while a copy follows the pointer (pointer-events: none, positioned by two CSS variables set from the handlers, not from render); the column under the pointer is read withelementFromPointand the landing place from the cards' vertical middles (dropIndex), drawn as a line. A press that moves under 5 px is a click and opens the task; the pointer is captured only once the press becomes a drag, so a drag's release never clicks the title. Escape puts the card back. Near the board's left or right edge a drag scrolls it.{title} 옮기기): 옮길 곳 — the other four columns — and 위로 / 아래로. A menu move says○○(으)로 옮겼어요.in the board's status line.rankBetween,placeTask), sent with theversionthe board read:PATCH {version, stage, rank}. It shows at once (optimistic); the hub's answer brings the task back with its new version. 409laf:task_stale(a run or a colleague moved it first) reads the board again and says방금 다른 사람이 이 업무를 바꿨어요. 업무판을 새로 불러왔어요.; any other refusal puts the card back and says why. A card's next drag waits for its last move's answer.hub.task.changed/hub.task.assignedframes make the board and that task's detail stale (hubKeysForFrame); nothing polls.task.changedandtask.assignedjoinHUB_EVENT_TYPES, which the VM's line filters by.bot:<me>), a teammate (m:<id>, 나 first), or one of this person's routines as a sub-Bot (job:<routine id>, shown as루틴 · <name>). Seen by the company (visibility: "company").?task=in the address): its description, who has it (changeable), its notes, a box for a note, and — when this VM's 라피 worked it —라피가 한 일 보기, the conversation feat(hub): 라피 takes the board's tasks given to it, works them as a routine runs, and hands them back #260 namestask:<task id>. A 라피's note is never shown as a code (taskNoteText):laf:task_ran … steps:4→ 라피가 4단계로 마쳤어요. 확인해 주세요. ·laf:task_needs_approval … topic:browser/activate@smartstore.naver.com→ 라피가 허락을 기다려요: smartstore.naver.com에서 버튼 누르기 ·needs_login→ 저장된 로그인이 없어서 라피가 멈췄어요. ·needs_person→ 사람이 봐야 해서 라피가 멈췄어요. ·run_failed→ 라피가 끝내지 못했어요. ·run_stopped→ 라피의 작업을 멈췄어요. · a code this build does not know → 라피가 남긴 메모. A stop is drawn on the accent background. A person's note is its own text.The doors (
server/src/hub/routes-ui-tasks.ts, its own file besideroutes-ui.ts, mounted at/api/hub, each behindrequireUser):/hub/v1)GET /api/hub/tasks?stage=&cursor=&limit=GET /tasksGET /api/hub/tasks/:idGET /tasks/:idPOST /api/hub/tasksPOST /tasksPATCH /api/hub/tasks/:idPATCH /tasks/:idPOST /api/hub/tasks/:id/notesPOST /tasks/:id/notesEvery call goes as
x-laf-origin: person; every write asas: "member"with no run id, whatever the body says —as: "bot", a run id and checkout/heartbeat/release are a 라피's run's, and none is a door here. Only the board's fields are forwarded, each checked (a stage the board has, an assignee spelledm:|bot:|job:<id>, ids that stay in their path, numbers that are numbers); aPATCHwithout aversionis refused unsent. Reads are asked once, writes keep the client's retries under the press's key. A 401 or 5xx from the hub is a 502 with its fact; 400/403/404/409/413/507 keep their status andlaf:code (solaf:task_stalereaches the board as a 409). Same rules as #256's doors, spelled the same way in this file so the board and the other parts change without touching each other.The room's working line (
components/company/room-working-line.tsx,lib/hub/room-working.ts). Under an open room's messages:이수진의 라피가 알아보는 중(on the asker's own screen내 라피가 알아보는 중) for each member who asked their own 라피 and has no answer from it after the question. And the room's box now sendsmentions: ["lafi"]when the text says@라피, which is what #259 answers — #256's box sent no mentions, so nothing could ask.The rule I chose for a failed or stopped turn. #259 gives the room no signal when the asker's turn fails or is stopped (it tells only the asker, in their own conversation), and its window is ten minutes — the line would hang that long over a question nobody is working on. So: on every screen the line shows for at most two minutes after the question (
ROOM_WORKING_SHOWN_MS); the 라피's answer ends it sooner. The asker's own screen knows more: their VM's working list says whether a room run (origin: "room") is going, and while one is, their line stays up to #259's ten minutes; once none is and two minutes have passed, it goes like everybody else's. The line ends at the moment its window passes (a timer to the soonest end), not on the next minute. The asker also gets내 라피 대화에서 보기, linking to their conversation for the room (room_<room id>_<bot id>, #259) once it exists. Trade-off, said plainly: a colleague watching a long answer (over two minutes) sees the line go before the answer arrives.The rule is copied, not imported. #259's
lafiWorkinglives inshared/hub/room-answers.tson its branch; merging #259 here would bring #255's tools and #259's engine changes into this PR. The rule (about 30 lines) is copied intolib/hub/room-working.tswith its own window and the asker's extension; at merge the two can become one (see below).Left out: the board reads up to five pages of 200 tasks; no filters (mine / a person's); no title or description editing after creation (the door forwards both, the screen does not offer it yet); visibility other than
companyis not offered when creating; nothing works ajob:task yet (no VM side picks one up — #260 takesbot:<me>only), so giving one to a routine only records it; a rank that runs out of float precision after ~50 halvings at one spot is not renumbered.For the merge
shared/hub/protocol.tsHUB_EVENT_TYPES: addstask.changed,task.assigned. feat(hub): the task board — tasks, notes, a run's claim on one that only one run wins, and the sweep that frees it #253 adds the same two (with its doc block); the resolution is the union, as with feat(app): 회사 — the company's rooms, files, skills, members and team memory, through the VM #256's list.app/src/lib/i18n-ko.ts:"{name}'s Lafi"and"{name}'s Lafi is looking into it"are added here and on feat(hub): "@라피" in a team room is answered by the asker's own VM, in a conversation for that room #259 with the same Korean. Whichever lands second must drop its copy (a duplicate key is a typecheck error, TS1117).app/src/lib/hub/room-working.tscopies feat(hub): "@라피" in a team room is answered by the asker's own VM, in a conversation for that room #259'slafiWorkingrule. After feat(hub): "@라피" in a team room is answered by the asker's own VM, in a conversation for that room #259 lands,roomAuthorcan be imported from@shared/hub/room-answersandLAFI_MENTIONfromHUB_LAFI_MENTION.app/tests/hub-company.test.ts: its "unknown kind" example washub.task.changed, which is now known; it ishub.vault.changed.app/src/routes/_authed/_app/company.tsx: the 업무판 tab,<Outlet />and the wide measure on/company/tasks; a sibling adding its own 회사 part touches the same tab list.app/src/routeTree.gen.tsregenerated;shared/screen-errors.tsgains/company/tasks.Where it runs
doneby a 라피 is refused by the hub; a person may set 끝, andlaf:task_done_by_personhas its words if it ever reaches the board. 업무판 is under 회사, which is drawn only where there is a company.Boundary and audit
x-laf-origin: person); this door refuses only malformed input (400laf:request_invalid), which acts on nothing.asis forced tomemberandrunIddropped; ids, stages, assignees and query fields are checked and spelled again.Korean
t()with its Korean entry in this change (next to the 회사 block, 49 entries).t()inswitches, andhub-tasks.test.tschecks each note's key has its Korean.The gate
On this branch's head,
LAF_TEST_DB_SUFFIX=w1 LAF_TEST_WORKERS=2: headbce4cb5c, 2026-10-17 16:16 KST: typecheck exit 0; biome lint no errors (the 2 warnings and 1 info are in files this PR does not touch); format clean;test:ciexit 0 — server 4830 / floor 4789, app 2825 / 2608, agent-computer 651 / 632, root 1780 / 1744, hub 48 / 48.hub-routes-ui-tasks.test.ts), app 2564 → 2608 (+44:hub-tasks.test.ts34,hub-room-working.test.ts10). The three files are inscripts/test-manifest.json.Proof
Live. A scratch hub built from this branch with #253 (
laf/hub-tasks) and #250 (laf/hub-rooms) merged in (theHUB_EVENT_TYPESlists unioned, rooms' migration renumbered0002), on :7400 with its own database; workspace 라프상사 and two members registered through the signedPOST /hub/v1/fleet(A 김민수 admin, B 이수진 member). Two product servers of this branch, each on its own database and pass (A :3250, B :3251,LAF_DEV_NO_AUTH), each with a Vite dev server of this branch (5250 → A, 5251 → B), headless Chrome (Playwrightchannel: "chrome"), ko-KR, 1280×820. Before each run the board was put back to a known state through A'sPATCHdoor. The needs-approval note was written to the hub with A's pass asas: "bot"— the line #260 writes (#260 is not in this stack), and the conversation link of a worked task could not be shown live for the same reason (taskConversationOfis tested).PATCHthrough A's server to the hub, the hub's event over B's server's line to B's socket, and B asking B's server, which asks the hub). Screenshots: A after the drags, B showing the move.재고 확인 / 매장 휴무 안내문 만들기 / 거래처 견적서 보내기, B the same in 27 ms, and the same after A reloads (the rank was kept by the hub).?task=t_…); after a drag no dialog; the next press opens it again.routeWebSocket), so it hears no frames and keeps the old version; A moves 3월 매출 정리 to 검토; B2 drags it to 막힘 → the hub answers 409laf:task_stale, B2 says 방금 다른 사람이 이 업무를 바꿨어요. 업무판을 새로 불러왔어요., and its board, read again, shows the card in 검토 (A's move), not 막힘. Screenshot.김민수의 라피 — 라피가 허락을 기다려요: smartstore.naver.com에서 버튼 누르기and김민수의 라피 — 라피가 4단계로 마쳤어요. 확인해 주세요.— no code on screen. Screenshot.내 라피, A's이수진의 라피, on A's page in 76 (93 in a second run) ms. Screenshot of the choices (아직 안 정함 · 내 라피 · 팀원: 나, 김민수).hasTouch), touch events through CDP (Input.dispatchTouchEvent, pointerTypetouch) on the grip of 재고 확인 → 막힘: A's page shows it there. (The first cut cancelled a finger's drag: the grip's icon holds a touch's implicit capture, and handing it to the card raisedlostpointercaptureon the icon, which bubbled to the card; now only the card's own capture ending cancels.)@라피 이번 주 매장 일정 정리해 줘in the room 매장 운영: A's page (fake clock installed) shows 이수진의 라피가 알아보는 중 in 30 ms; B's shows 내 라피가 알아보는 중. At 1:50 after the question A still shows it; at 2:05 it is gone (no answer came: feat(hub): "@라피" in a team room is answered by the asker's own VM, in a conversation for that room #259 is not in this stack, which is the failed-turn case). Screenshots.GET /favicon.ico404 from the Vite dev server (not this change), and B2's own 409 in step 5 (the stale move, expected).Each test seen failing with what it guards reverted:
body: {...body, as: body.as ?? "member"}forwarded onPOST /tasks→ 5 ofhub-routes-ui-tasks.test.tsfail.case 409removed fromstatusFor→ "a stale move keeps the hub's 409" fails.versioncheck onPATCHremoved → "a move with no version is refused unsent" fails.rankBetweenansweringbeforeinstead of the midpoint → 2 fail; the "already there" check inplaceTaskremoved → 2 fail.laf:task_needs_approvalcase renamed → 2 fail;laf:task_run_failed→ 1 fails.hub.task.changedcase inhubKeysForFramerenamed → 1 fails.hub-room-working.test.tsfail; the "answered after" check removed → 2 fail.Model calls: no chat turns. Each server's boot made its two usual warm-up calls (4 calls, 1,165 + 732 input tokens each) — well under $0.01.
🤖 Generated with Claude Code