{"version":3,"file":"team-focus.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/team-focus.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAiB,GAAG,EAAE,MAAM,0BAA0B,CAAC;AACzF,OAAO,KAAK,EAAE,WAAW,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAG5F,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAElE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAGrE,gEAAgE;AAChE,MAAM,WAAW,aAAa;IAC7B,EAAE,EAAE,GAAG,CAAC;IACR,SAAS,EAAE,kBAAkB,CAAC;IAC9B,eAAe,EAAE,SAAS,CAAC;IAC3B,wEAAwE;IACxE,SAAS,IAAI,SAAS,CAAC;IACvB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,cAAc,CAAC,SAAS,EAAE,WAAW,EAAE,EAAE,IAAI,CAAC,EAAE,wBAAwB,GAAG,OAAO,CAAC,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;IACzG,wDAAwD;IACxD,gBAAgB,IAAI,OAAO,CAAC;CAC5B;AAED,qBAAa,mBAAmB;IAKnB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAJjC,OAAO,CAAC,MAAM,CAA6C;IAC3D,OAAO,CAAC,WAAW,CAAmD;IACtE,OAAO,CAAC,YAAY,CAAwC;IAE5D,YAA6B,IAAI,EAAE,aAAa,EAAI;IAEpD,0DAA0D;IAC1D,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED;;;OAGG;IACH,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAkB7C;YAUa,eAAe;IA6B7B,2DAA2D;IAC3D,UAAU,IAAI,IAAI,CAYjB;IAED,8EAA8E;IAC9E,SAAS,IAAI,IAAI,CAIhB;IAED;;;;OAIG;IACH,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAsCjC;IAED,+DAA+D;IAC/D,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAuB7B;IAED,+EAA+E;IAC/E,WAAW,IAAI,IAAI,CAKlB;CACD","sourcesContent":["/**\n * hoocode team focus (--team): focus role rows, nudge, attach.\n *\n * Wires a hooteams connection into the TUI: team focus (app.team.focus),\n * nudging (n), the attach side panel (a) on the task panel's teams view, and\n * approval gates. task_paused events (and gates already pending on the server)\n * surface inline in the attach panel when it shows the paused role, otherwise\n * in the options pane; the answer goes back over POST /tasks/:id/resume.\n */\n\nimport type { Component, Container, OverlayHandle, TUI } from \"@kolisachint/hoocode-tui\";\nimport type { AskQuestion, ExtensionUIDialogOptions } from \"../../core/extensions/index.js\";\nimport { taskStore } from \"../../core/task-store.js\";\nimport { type TeamApproval, TeamApprovalCoordinator } from \"../../core/team-approvals.js\";\nimport type { TeamViewConnection } from \"../../core/team-view.js\";\nimport { ExtensionInputComponent } from \"./components/extension-input.js\";\nimport type { TaskPanelComponent } from \"./components/task-panel.js\";\nimport { TeamAttachPanelComponent } from \"./components/team-attach-panel.js\";\n\n/** The slice of the interactive mode the team feature needs. */\nexport interface TeamFocusDeps {\n\tui: TUI;\n\ttaskPanel: TaskPanelComponent;\n\teditorContainer: Container;\n\t/** The prompt editor (read at call time; the editor can be swapped). */\n\tgetEditor(): Component;\n\tshowStatus(message: string): void;\n\tshowWarning(message: string): void;\n\tshowAskOptions(questions: AskQuestion[], opts?: ExtensionUIDialogOptions): Promise<string[] | undefined>;\n\t/** True while another ask-options pane is on screen. */\n\tisAskOptionsOpen(): boolean;\n}\n\nexport class TeamFocusController {\n\tprivate client: TeamViewConnection | undefined = undefined;\n\tprivate attachPanel: TeamAttachPanelComponent | undefined = undefined;\n\tprivate attachHandle: OverlayHandle | undefined = undefined;\n\n\tconstructor(private readonly deps: TeamFocusDeps) {}\n\n\t/** True once a team client has been attached (--team). */\n\tget connected(): boolean {\n\t\treturn this.client !== undefined;\n\t}\n\n\t/**\n\t * Wire a hooteams connection into the TUI. Called by main.ts when `--team`\n\t * is set, before run().\n\t */\n\tattachClient(client: TeamViewConnection): void {\n\t\tthis.client = client;\n\t\tconst approvals = new TeamApprovalCoordinator({\n\t\t\tpresent: (approval, signal) => this.presentApproval(approval, signal),\n\t\t\tresume: (taskId, option) => client.resume(taskId, option),\n\t\t\tinfo: (message) => this.deps.showStatus(message),\n\t\t\twarn: (message) => this.deps.showWarning(message),\n\t\t});\n\t\tclient.subscribe((event) => approvals.handleEvent(event));\n\t\t// Gates that opened before we attached don't replay as task_paused.\n\t\tvoid client.pendingApprovals().then(\n\t\t\t(pending) => {\n\t\t\t\tfor (const gate of pending) approvals.enqueuePending(gate);\n\t\t\t},\n\t\t\t() => {\n\t\t\t\t// Best-effort like the rest of the bridge; live gates still arrive via SSE.\n\t\t\t},\n\t\t);\n\t}\n\n\t/**\n\t * Show one team approval gate and resolve with the chosen (or free-form)\n\t * answer, undefined when skipped. When the attach side panel is open on the\n\t * role that paused, the gate renders inline in the panel — right where its\n\t * stream stopped; otherwise it goes to the options pane, waiting politely\n\t * while another ask is on screen. Either way the signal (gate answered\n\t * elsewhere) dismisses the prompt.\n\t */\n\tprivate async presentApproval(approval: TeamApproval, signal: AbortSignal): Promise<string | undefined> {\n\t\tconst panel = this.attachPanel;\n\t\tif (panel && approval.role === panel.role) {\n\t\t\tthis.attachHandle?.focus();\n\t\t\tconst answer = await panel.presentApproval(approval, signal);\n\t\t\t// Detaching mid-gate settles as skipped — fall through to the options\n\t\t\t// pane so the question isn't silently lost. A skip with the panel\n\t\t\t// still open is a real skip.\n\t\t\tif (answer !== undefined || signal.aborted || this.attachPanel === panel) return answer;\n\t\t}\n\t\twhile (this.deps.isAskOptionsOpen() && !signal.aborted) {\n\t\t\tawait new Promise((resolve) => setTimeout(resolve, 200));\n\t\t}\n\t\tif (signal.aborted) return undefined;\n\t\tconst answers = await this.deps.showAskOptions(\n\t\t\t[\n\t\t\t\t{\n\t\t\t\t\tquestion: approval.question,\n\t\t\t\t\tshort: approval.taskId,\n\t\t\t\t\tdetail: `team task \"${approval.taskId}\"${approval.role ? ` (${approval.role})` : \"\"} is paused until answered`,\n\t\t\t\t\toptions: approval.options.map((label) => ({ label })),\n\t\t\t\t\tallowCustom: true,\n\t\t\t\t},\n\t\t\t],\n\t\t\t{ signal },\n\t\t);\n\t\treturn answers?.[0];\n\t}\n\n\t/** Move keyboard focus to the task panel's team roster. */\n\tenterFocus(): void {\n\t\tif (!this.client) {\n\t\t\tthis.deps.showStatus(\"No team connected. Start with --team <url> to mirror a hooteams server.\");\n\t\t\treturn;\n\t\t}\n\t\tif (!taskStore.agents().some((a) => a.kind === \"role\")) {\n\t\t\tthis.deps.showStatus(\"Team roster is empty — waiting for roles from the team server.\");\n\t\t\treturn;\n\t\t}\n\t\tthis.deps.taskPanel.setView(\"teams\");\n\t\tthis.deps.ui.setFocus(this.deps.taskPanel);\n\t\tthis.deps.ui.requestRender();\n\t}\n\n\t/** Leave team focus: detach any side panel and return focus to the editor. */\n\texitFocus(): void {\n\t\tthis.closeAttach();\n\t\tthis.deps.ui.setFocus(this.deps.getEditor());\n\t\tthis.deps.ui.requestRender();\n\t}\n\n\t/**\n\t * Inline nudge editor for a role (n in team focus or while attached).\n\t * Swaps the prompt editor for a one-line input; submit fires POST /steer in\n\t * the background (the REPL and team focus are never blocked on the network).\n\t */\n\tshowNudgeInput(role: string): void {\n\t\tconst client = this.client;\n\t\tif (!client) return;\n\n\t\tconst restoreFocus = () => {\n\t\t\tthis.deps.editorContainer.clear();\n\t\t\tthis.deps.editorContainer.addChild(this.deps.getEditor());\n\t\t\t// Return focus where the nudge came from: the attach panel if one is\n\t\t\t// open, otherwise the team roster.\n\t\t\tif (this.attachHandle) this.attachHandle.focus();\n\t\t\telse this.deps.ui.setFocus(this.deps.taskPanel);\n\t\t\tthis.deps.ui.requestRender();\n\t\t};\n\n\t\tconst input = new ExtensionInputComponent(\n\t\t\t`Nudge ${role}`,\n\t\t\tundefined,\n\t\t\t(value) => {\n\t\t\t\tinput.dispose();\n\t\t\t\trestoreFocus();\n\t\t\t\tconst message = value.trim();\n\t\t\t\tif (!message) return;\n\t\t\t\tvoid client.steer(role, message).then(\n\t\t\t\t\t() => this.deps.showStatus(`Nudged ${role}`),\n\t\t\t\t\t(error) => this.deps.showWarning(`Failed to nudge ${role}: ${String(error)}`),\n\t\t\t\t);\n\t\t\t},\n\t\t\t() => {\n\t\t\t\tinput.dispose();\n\t\t\t\trestoreFocus();\n\t\t\t},\n\t\t\t{ tui: this.deps.ui },\n\t\t);\n\n\t\tthis.deps.editorContainer.clear();\n\t\tthis.deps.editorContainer.addChild(input);\n\t\tthis.deps.ui.setFocus(input);\n\t\tthis.deps.ui.requestRender();\n\t}\n\n\t/** Open the attach side panel for a role (a in team focus). */\n\tshowAttach(role: string): void {\n\t\tconst client = this.client;\n\t\tif (!client) return;\n\t\t// One attached role at a time: re-attaching swaps the panel.\n\t\tthis.closeAttach();\n\t\tconst panel = new TeamAttachPanelComponent(\n\t\t\trole,\n\t\t\tclient,\n\t\t\t{\n\t\t\t\tonDetach: () => this.closeAttach(),\n\t\t\t\tonNudge: (attachedRole) => this.showNudgeInput(attachedRole),\n\t\t\t},\n\t\t\tthis.deps.ui,\n\t\t);\n\t\tthis.attachPanel = panel;\n\t\t// preFocus is the task panel (attach is triggered from team focus), so\n\t\t// hiding the overlay drops the user back on the role roster.\n\t\tthis.attachHandle = this.deps.ui.showOverlay(panel, {\n\t\t\tanchor: \"top-right\",\n\t\t\twidth: \"45%\",\n\t\t\tminWidth: 36,\n\t\t\tmargin: { top: 1, right: 1 },\n\t\t});\n\t}\n\n\t/** Detach the side panel; the role keeps running. Safe to call when closed. */\n\tcloseAttach(): void {\n\t\tthis.attachPanel?.dispose();\n\t\tthis.attachHandle?.hide();\n\t\tthis.attachPanel = undefined;\n\t\tthis.attachHandle = undefined;\n\t}\n}\n"]}