-
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
base: main
Are you sure you want to change the base?
Changes from all commits
7c7e17f
76de5a9
ea8ce8f
0cd5f6d
432dd4f
6d91a3e
fdfc9a3
910710e
7207840
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,108 @@ | |
| // 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 | ||
|
|
||
| // Capture the set of pending sessions at install-time so an install | ||
| // failure drains EVERY caller that awaited this install, not just the | ||
| // one whose sessionID we happen to be handling. Later waiters would | ||
| // otherwise see success from the shared promise and stop retrying, | ||
| // leaving permanently-stale entries in the pending set. (cubic round 3.) | ||
| const armingSessions = new Set(pendingWorkspacePromptSessions) | ||
|
|
||
| workspacePromptInstall = (async () => { | ||
| try { | ||
| const unsubscribe = await AppRuntime.runPromise( | ||
| EventV2Bridge.Service.use((events) => | ||
| events.listen((event) => | ||
| Effect.gen(function* () { | ||
| if (event.type !== SessionEvent.Idle.type) return | ||
|
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]: Subscribing to the deprecated
Reply with |
||
| const sid = (event.data as { sessionID?: string } | undefined)?.sessionID | ||
| if (!sid || !pendingWorkspacePromptSessions.has(sid)) return | ||
| pendingWorkspacePromptSessions.delete(sid) | ||
| yield* events.publish(TuiEvent.CommandExecute, { | ||
| command: "altimate.workspace.postScan", | ||
| }) | ||
|
Comment on lines
+81
to
+83
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 — drop every session that was waiting on this install | ||
| // so the next scan retries from scratch. Dropping only the current | ||
| // caller's ID would leave later waiters (already resolved by the | ||
| // shared install promise) with permanently-stale pending entries. | ||
| // (cubic round 3.) | ||
| for (const sid of armingSessions) pendingWorkspacePromptSessions.delete(sid) | ||
| 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 +236,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.