-
Notifications
You must be signed in to change notification settings - Fork 133
feat: post-scan Workspaces prompt + altimate-code link subcommand
#1099
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9479cf2
4d66327
7da50b0
30f7442
fdb6e37
4c7240c
9f77a5e
5959200
270907e
ae676de
5d97559
f046b66
3427b2c
12e47f5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,112 @@ | |
| // session loop. | ||
| import type { Hooks, PluginInput } from "@opencode-ai/plugin" | ||
| import * as OnboardingTelemetry from "../telemetry/onboarding" | ||
| // altimate_change start — AI-8398 workspaces trigger. Reaches into the same | ||
| // EventV2 bridge the server/routes/tui.ts uses to publish TuiEvent.CommandExecute | ||
| // so the workspace TuiPlugin (packages/opencode/src/plugin/tui/altimate/workspace.tsx) | ||
| // runs its post-scan flow. Feature-flagged via Flag.ALTIMATE_WORKSPACE. | ||
| import { Effect } from "effect" | ||
| import { Flag } from "@opencode-ai/core/flag/flag" | ||
| import { AltimateApi } from "@/altimate/api/client" | ||
| import { AppRuntime } from "@/effect/app-runtime" | ||
| import { EventV2Bridge } from "@/event-v2-bridge" | ||
| import { TuiEvent } from "@/server/tui-event" | ||
| import { Event as SessionEvent } from "@/session/status" | ||
| import { Log } from "@/altimate/util/log" | ||
|
|
||
| const workspaceLog = Log.create({ service: "altimate-workspace" }) | ||
|
|
||
| /** | ||
| * Publish the workspace-postScan command AFTER the session goes idle, not on | ||
| * `project_scan`'s tool.execute.after. Rationale: project_scan tool RETURNS while | ||
| * the LLM is still generating the activation-menu text; the dialog paints in | ||
| * that window but user interactions queue behind the streaming. Waiting for | ||
| * session.idle costs a few seconds of latency but sidesteps the race entirely — | ||
| * the dialog appears once things are quiet. | ||
| * | ||
| * One-shot per sessionID: pending sessions live in a Set, and when a session | ||
| * emits idle its id is removed. When the Set drains, the EventV2 listener is | ||
| * torn down via the unsubscribe returned by ``events.listen()`` so a | ||
| * permanently-installed no-op handler isn't left behind for the process | ||
| * lifetime (m4 in the consensus review). A pending arm is dropped if a | ||
| * second project_scan fires in the same session. | ||
| */ | ||
| const pendingWorkspacePromptSessions = new Set<string>() | ||
| /** The unsubscribe returned by ``events.listen()`` is an Effect (not a plain | ||
| * function) — running it removes the listener. Store the Effect and execute | ||
| * it through ``AppRuntime.runPromise`` on teardown; earlier code cast it to | ||
| * ``() => void`` and called it directly, which threw because Effects are not | ||
| * callable as functions. (cubic-dev-ai round 3.) */ | ||
| let workspacePromptUnsubscribe: Effect.Effect<void, never, never> | null = null | ||
| // Guard against two concurrent scans passing the ``!workspacePromptUnsubscribe`` | ||
| // check before either install completes — both would then install a listener | ||
| // and the later assignment would overwrite the first disposer, leaking the | ||
| // first listener for the process lifetime. Store the in-flight install as a | ||
| // shared promise so concurrent callers await the same result. (CR round 2.) | ||
| let workspacePromptInstall: Promise<void> | null = null | ||
|
|
||
| async function armWorkspacePromptOnSessionIdle(sessionID: string): Promise<void> { | ||
| pendingWorkspacePromptSessions.add(sessionID) | ||
| if (workspacePromptUnsubscribe) return | ||
| if (workspacePromptInstall) return workspacePromptInstall | ||
|
|
||
| workspacePromptInstall = (async () => { | ||
| try { | ||
| const unsubscribe = await AppRuntime.runPromise( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (P2): Fix: replace
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in |
||
| EventV2Bridge.Service.use((events) => | ||
| events.listen((event) => | ||
| Effect.gen(function* () { | ||
| // Subscribe to ``Event.Status`` (session/status.ts:42, defined | ||
| // via ``EventV2.define``) rather than ``Event.Idle`` — the | ||
| // latter is marked ``// deprecated`` at session/status.ts:49 | ||
| // and is only kept around for the legacy Bus SSE mirror at | ||
| // session/status.ts:176. Filtering ``event.data.status.type | ||
| // === "idle"`` gives us the same trigger without riding the | ||
| // deprecated event. (harness-bot round 8.) | ||
| if (event.type !== SessionEvent.Status.type) return | ||
| const data = event.data as | ||
| | { sessionID?: string; status?: { type?: string } } | ||
| | undefined | ||
| if (data?.status?.type !== "idle") return | ||
| const sid = data.sessionID | ||
| if (!sid || !pendingWorkspacePromptSessions.has(sid)) return | ||
| pendingWorkspacePromptSessions.delete(sid) | ||
| yield* events.publish(TuiEvent.CommandExecute, { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thought: The migration path is to check |
||
| command: "altimate.workspace.postScan", | ||
| }) | ||
|
Comment on lines
+85
to
+87
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the scan runs through a server hosting multiple projects or an explicit opencode workspace, this listener republishes the command without AGENTS.md reference: packages/opencode/AGENTS.md:L127-L129 Useful? React with 👍 / 👎. |
||
| // Once the Set drains, tear the listener down. A later scan | ||
| // that adds a new pending session re-arms it from scratch. | ||
| // ``teardown`` is an Effect — run it through the app runtime, | ||
| // don't call it as a function. (cubic round 3.) | ||
| if (pendingWorkspacePromptSessions.size === 0 && workspacePromptUnsubscribe) { | ||
| const teardown = workspacePromptUnsubscribe | ||
| workspacePromptUnsubscribe = null | ||
| AppRuntime.runPromise(teardown).catch((err) => { | ||
| workspaceLog.warn("session-idle listener teardown failed", { | ||
| err: String(err), | ||
| }) | ||
| }) | ||
| } | ||
| }), | ||
| ), | ||
| ), | ||
| ) | ||
| workspacePromptUnsubscribe = unsubscribe | ||
| } catch (err) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [SUGGESTION]: The install-failure machinery is effectively unreachable — simplify
Reply with |
||
| // Install failed — drain EVERY pending session, not just the ones | ||
| // snapshotted at install-time. A late-arriving ``armWorkspacePromptOn | ||
| // SessionIdle`` between the snapshot and the failure would otherwise | ||
| // be a permanent orphan (its sessionID stays in the pending set but | ||
| // no listener will ever fire for it). (harness-bot round 8.) | ||
| pendingWorkspacePromptSessions.clear() | ||
| workspaceLog.warn("session-idle listener install failed", { err: String(err) }) | ||
| } finally { | ||
| workspacePromptInstall = null | ||
| } | ||
| })() | ||
| return workspacePromptInstall | ||
| } | ||
| // altimate_change end | ||
|
|
||
| const ONBOARD_CONNECT = "onboard-connect" | ||
|
|
||
|
|
@@ -134,6 +240,14 @@ export async function OnboardingTelemetryPlugin(_input: PluginInput): Promise<Ho | |
| }, | ||
| input.sessionID, | ||
| ) | ||
| // altimate_change start — AI-8398 workspaces post-scan prompt trigger. | ||
| // ARM (don't publish yet) — the dialog fires when the session goes idle, | ||
| // not the moment project_scan returns. See armWorkspacePromptOnSessionIdle | ||
| // above for why the immediate publish raced the LLM's ongoing streaming. | ||
| if (Flag.ALTIMATE_WORKSPACE && (await AltimateApi.isConfigured().catch(() => false))) { | ||
| void armWorkspacePromptOnSessionIdle(input.sessionID) | ||
| } | ||
| // altimate_change end | ||
| return | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.