@xfey/tutti 0.1.21 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/collaboration-state/index.d.ts +1 -1
  2. package/dist/collaboration-state/index.js +1 -1
  3. package/dist/collaboration-state/messages.js +50 -1
  4. package/dist/collaboration-state/scratchpad.d.ts +2 -0
  5. package/dist/collaboration-state/scratchpad.js +63 -0
  6. package/dist/collaboration-state/storage-types.d.ts +5 -0
  7. package/dist/collaboration-state/types.d.ts +8 -1
  8. package/dist/control-plane/scratchpad-refresh-start.d.ts +2 -0
  9. package/dist/control-plane/scratchpad-refresh-start.js +10 -0
  10. package/dist/control-plane/scratchpad-refresh.d.ts +2 -0
  11. package/dist/control-plane/scratchpad-refresh.js +1 -0
  12. package/dist/project-timeline/index.d.ts +17 -3
  13. package/dist/server-shell/cli/host-lifecycle.js +2 -19
  14. package/dist/server-shell/cli/launch-command.d.ts +11 -0
  15. package/dist/server-shell/cli/launch-command.js +42 -6
  16. package/dist/server-shell/cli/terminal-qr.d.ts +3 -0
  17. package/dist/server-shell/cli/terminal-qr.js +17 -0
  18. package/dist/server-shell/http/routes/project-api/openapi-collaboration-routes.d.ts +1 -0
  19. package/dist/server-shell/http/routes/project-api/openapi-timeline-routes.d.ts +94 -5
  20. package/dist/server-shell/http/routes/project-api/openapi.d.ts +95 -5
  21. package/dist/server-shell/http/routes/project-api/payload-validation.d.ts +6 -2
  22. package/dist/server-shell/http/routes/project-api/payload-validation.js +3 -0
  23. package/dist/server-shell/http/routes/project-api/project-timeline-projection.js +694 -502
  24. package/migrations/0013_scratchpad_receipt_sources.sql +24 -0
  25. package/migrations/README.md +3 -2
  26. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -1
  27. package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -1
  28. package/node_modules/@tutti/shared/dist/schemas/api/messages.d.ts +1 -0
  29. package/node_modules/@tutti/shared/dist/schemas/api/messages.js +1 -0
  30. package/node_modules/@tutti/shared/dist/schemas/api/project-timeline.d.ts +313 -12
  31. package/node_modules/@tutti/shared/dist/schemas/api/project-timeline.js +131 -14
  32. package/node_modules/@tutti/shared/dist/schemas/api/types.d.ts +5 -1
  33. package/package.json +1 -1
  34. package/web/assets/index-BfFYfzeO.js +29 -0
  35. package/web/assets/index-De6mK5Q2.css +1 -0
  36. package/web/index.html +2 -2
  37. package/web/assets/index-BtneECwp.js +0 -29
  38. package/web/assets/index-CM9mbbeW.css +0 -1
@@ -1,7 +1,7 @@
1
1
  export { CollaborationStateCursorError } from "./serialization.js";
2
2
  export { createIdleExecutionStatusProjection } from "./execution-status.js";
3
3
  export { createMainChatAgentMessage, createMainChatMessage, createMainChatSystemMessage, hasMainChatWorklistFeedbackMessage, readCompletedWorklistFeedbackTaskIds, readMessageProjectionById, readMainChatMessageWindow, readMainChatMessagesAfter, readMainChatMessagesAfterCursor, readMainChatMessagesBeforeCursor, } from "./messages.js";
4
- export { createEmptyScratchpadProjection, readScratchpadProjection, submitScratchpadProjection, upsertScratchpadProjection, } from "./scratchpad.js";
4
+ export { createEmptyScratchpadProjection, readScratchpadReceiptSourceRefs, readScratchpadProjection, submitScratchpadProjection, upsertScratchpadProjection, } from "./scratchpad.js";
5
5
  export { advanceScratchpadTaskCompileSourceCursor, markScratchpadRefreshFailed, markScratchpadRefreshStarted, markScratchpadRefreshSucceeded, markScratchpadSourceDirty, readScratchpadSourceState, } from "./scratchpad-source-state.js";
6
6
  export { deleteStaleClarificationSubmitReplayHints, readActiveClarificationProjection, readClarificationRoundProjection, readClarificationRoundsForThread, readClarificationThreadResponse, readSealedClarificationRoundsWithoutSuccessor, readSubmittedTaskCompileClarificationRounds, readSubmittedTaskCompileClarificationRoundsAfterCursor, } from "./clarification-records.js";
7
7
  export { createClarificationRoundAgentMessage, createClarificationRoundMessage, readClarificationRoundMessagesWindow, } from "./clarification-messages.js";
@@ -1,7 +1,7 @@
1
1
  export { CollaborationStateCursorError } from "./serialization.js";
2
2
  export { createIdleExecutionStatusProjection } from "./execution-status.js";
3
3
  export { createMainChatAgentMessage, createMainChatMessage, createMainChatSystemMessage, hasMainChatWorklistFeedbackMessage, readCompletedWorklistFeedbackTaskIds, readMessageProjectionById, readMainChatMessageWindow, readMainChatMessagesAfter, readMainChatMessagesAfterCursor, readMainChatMessagesBeforeCursor, } from "./messages.js";
4
- export { createEmptyScratchpadProjection, readScratchpadProjection, submitScratchpadProjection, upsertScratchpadProjection, } from "./scratchpad.js";
4
+ export { createEmptyScratchpadProjection, readScratchpadReceiptSourceRefs, readScratchpadProjection, submitScratchpadProjection, upsertScratchpadProjection, } from "./scratchpad.js";
5
5
  export { advanceScratchpadTaskCompileSourceCursor, markScratchpadRefreshFailed, markScratchpadRefreshStarted, markScratchpadRefreshSucceeded, markScratchpadSourceDirty, readScratchpadSourceState, } from "./scratchpad-source-state.js";
6
6
  export { deleteStaleClarificationSubmitReplayHints, readActiveClarificationProjection, readClarificationRoundProjection, readClarificationRoundsForThread, readClarificationThreadResponse, readSealedClarificationRoundsWithoutSuccessor, readSubmittedTaskCompileClarificationRounds, readSubmittedTaskCompileClarificationRoundsAfterCursor, } from "./clarification-records.js";
7
7
  export { createClarificationRoundAgentMessage, createClarificationRoundMessage, readClarificationRoundMessagesWindow, } from "./clarification-messages.js";
@@ -245,10 +245,59 @@ export function readMainChatMessageWindow(db, options = {}) {
245
245
  if (before !== null && after !== null) {
246
246
  throw new Error("before and after cursors cannot be used together");
247
247
  }
248
+ if (options.around !== undefined && (before !== null || after !== null)) {
249
+ throw new Error("around cannot be used with before or after cursors");
250
+ }
248
251
  let rows;
249
252
  let hasOlder = false;
250
253
  let hasNewer = false;
251
- if (before !== null) {
254
+ if (options.around !== undefined) {
255
+ const target = db
256
+ .prepare(`
257
+ SELECT *
258
+ FROM messages
259
+ WHERE scope_kind = 'main_chat'
260
+ AND id = ?
261
+ `)
262
+ .get(options.around);
263
+ if (target === undefined) {
264
+ rows = [];
265
+ }
266
+ else {
267
+ const beforeLimit = Math.floor((limit - 1) / 2);
268
+ const afterLimit = Math.max(0, limit - beforeLimit - 1);
269
+ const olderRows = db
270
+ .prepare(`
271
+ SELECT *
272
+ FROM messages
273
+ WHERE scope_kind = 'main_chat'
274
+ AND (
275
+ created_at < ?
276
+ OR (created_at = ? AND id < ?)
277
+ )
278
+ ORDER BY created_at DESC, id DESC
279
+ LIMIT ?
280
+ `)
281
+ .all(target.created_at, target.created_at, target.id, beforeLimit + 1);
282
+ const newerRows = db
283
+ .prepare(`
284
+ SELECT *
285
+ FROM messages
286
+ WHERE scope_kind = 'main_chat'
287
+ AND (
288
+ created_at > ?
289
+ OR (created_at = ? AND id > ?)
290
+ )
291
+ ORDER BY created_at ASC, id ASC
292
+ LIMIT ?
293
+ `)
294
+ .all(target.created_at, target.created_at, target.id, afterLimit + 1);
295
+ hasOlder = olderRows.length > beforeLimit;
296
+ hasNewer = newerRows.length > afterLimit;
297
+ rows = [...olderRows.slice(0, beforeLimit).reverse(), target, ...newerRows.slice(0, afterLimit)];
298
+ }
299
+ }
300
+ else if (before !== null) {
252
301
  rows = db
253
302
  .prepare(`
254
303
  SELECT *
@@ -1,8 +1,10 @@
1
1
  import type { ScratchpadProjection } from "@tutti/shared/schemas/api";
2
2
  import type { SqliteDatabase } from "../store/index.js";
3
+ import type { ScratchpadSourceRefRow } from "./storage-types.js";
3
4
  import type { ScratchpadSnapshotInput, SubmitScratchpadInput } from "./types.js";
4
5
  export declare function createEmptyScratchpadProjection(): ScratchpadProjection;
5
6
  export declare function upsertScratchpadProjection(db: SqliteDatabase, input: ScratchpadSnapshotInput): ScratchpadProjection;
6
7
  export declare function submitScratchpadProjection(db: SqliteDatabase, input: SubmitScratchpadInput): ScratchpadProjection;
7
8
  export declare function readScratchpadProjection(db: SqliteDatabase): ScratchpadProjection;
9
+ export declare function readScratchpadReceiptSourceRefs(db: SqliteDatabase, workflowRef: string): ScratchpadSourceRefRow[];
8
10
  //# sourceMappingURL=scratchpad.d.ts.map
@@ -1,5 +1,56 @@
1
1
  import { nowIso, parseJson, stringifyJson } from "./serialization.js";
2
2
  const SCRATCHPAD_RECEIPT_LIMIT = 100;
3
+ function normalizeSourceRefs(sourceRefs = []) {
4
+ const byMessageId = new Map();
5
+ for (const sourceRef of sourceRefs) {
6
+ if (sourceRef.message_id.trim() === "" || sourceRef.created_at.trim() === "") {
7
+ continue;
8
+ }
9
+ byMessageId.set(sourceRef.message_id, sourceRef);
10
+ }
11
+ return [...byMessageId.values()].sort((left, right) => {
12
+ const timeOrder = left.created_at.localeCompare(right.created_at);
13
+ return timeOrder === 0 ? left.message_id.localeCompare(right.message_id) : timeOrder;
14
+ });
15
+ }
16
+ function replaceCurrentScratchpadSourceRefs(db, sourceRefs) {
17
+ db.prepare("DELETE FROM scratchpad_source_refs WHERE scratchpad_id = 'current'").run();
18
+ const normalized = normalizeSourceRefs(sourceRefs);
19
+ if (normalized.length === 0) {
20
+ return;
21
+ }
22
+ const insert = db.prepare(`
23
+ INSERT INTO scratchpad_source_refs (
24
+ scratchpad_id,
25
+ message_id,
26
+ source_scope,
27
+ created_at
28
+ )
29
+ VALUES ('current', ?, ?, ?)
30
+ `);
31
+ for (const sourceRef of normalized) {
32
+ insert.run(sourceRef.message_id, sourceRef.source_scope, sourceRef.created_at);
33
+ }
34
+ }
35
+ function copyCurrentScratchpadSourcesToReceipt(db, workflowRef) {
36
+ db.prepare("DELETE FROM scratchpad_receipt_sources WHERE workflow_ref = ?").run(workflowRef);
37
+ db.prepare(`
38
+ INSERT INTO scratchpad_receipt_sources (
39
+ workflow_ref,
40
+ message_id,
41
+ source_scope,
42
+ created_at
43
+ )
44
+ SELECT
45
+ ?,
46
+ message_id,
47
+ source_scope,
48
+ created_at
49
+ FROM scratchpad_source_refs
50
+ WHERE scratchpad_id = 'current'
51
+ ORDER BY created_at ASC, message_id ASC
52
+ `).run(workflowRef);
53
+ }
3
54
  export function createEmptyScratchpadProjection() {
4
55
  return {
5
56
  id: "current",
@@ -76,6 +127,7 @@ export function upsertScratchpadProjection(db, input) {
76
127
  submitted_by_workflow_ref = NULL,
77
128
  submitted_at = NULL
78
129
  `).run(input.topic, input.background_summary, stringifyJson(input.current_consensus), stringifyJson(input.open_questions), stringifyJson(input.task_changes), input.updated_by_activity_ref ?? null, updatedAt);
130
+ replaceCurrentScratchpadSourceRefs(db, input.source_refs);
79
131
  return readScratchpadProjection(db);
80
132
  }
81
133
  export function submitScratchpadProjection(db, input) {
@@ -105,6 +157,7 @@ export function submitScratchpadProjection(db, input) {
105
157
  task_changes_json = excluded.task_changes_json,
106
158
  submitted_at = excluded.submitted_at
107
159
  `).run(input.workflow_ref, input.activity_ref, scratchpad.topic, scratchpad.background_summary, stringifyJson(scratchpad.current_consensus), stringifyJson(scratchpad.open_questions), stringifyJson(scratchpad.task_changes), submittedAt);
160
+ copyCurrentScratchpadSourcesToReceipt(db, input.workflow_ref);
108
161
  db.prepare(`
109
162
  UPDATE scratchpad
110
163
  SET
@@ -168,4 +221,14 @@ export function readScratchpadProjection(db) {
168
221
  }
169
222
  return scratchpad;
170
223
  }
224
+ export function readScratchpadReceiptSourceRefs(db, workflowRef) {
225
+ return db
226
+ .prepare(`
227
+ SELECT message_id, source_scope, created_at
228
+ FROM scratchpad_receipt_sources
229
+ WHERE workflow_ref = ?
230
+ ORDER BY created_at ASC, message_id ASC
231
+ `)
232
+ .all(workflowRef);
233
+ }
171
234
  //# sourceMappingURL=scratchpad.js.map
@@ -35,6 +35,11 @@ export type ScratchpadReceiptRow = {
35
35
  task_changes_json: string;
36
36
  submitted_at: string;
37
37
  };
38
+ export type ScratchpadSourceRefRow = {
39
+ message_id: MessageId;
40
+ source_scope: "main_chat" | "clarification_round";
41
+ created_at: string;
42
+ };
38
43
  export type ScratchpadSourceStateRow = {
39
44
  main_chat_cursor_created_at: string | null;
40
45
  main_chat_cursor_message_id: string | null;
@@ -1,6 +1,6 @@
1
1
  import type { RunLineage } from "@tutti/shared/domain";
2
2
  import type { AccountAvatarProjection } from "@tutti/shared/domain";
3
- import type { ActivityRef, ClarificationRoundRef, TaskId, TaskModuleId, WorkflowInvocationRef } from "@tutti/shared/ids";
3
+ import type { ActivityRef, ClarificationRoundRef, MessageId, TaskId, TaskModuleId, WorkflowInvocationRef } from "@tutti/shared/ids";
4
4
  import type { ClarificationRequestPayload, ClarificationRoundProjection, ClarificationSuccessorRef, ClarificationThreadProjection, MessageProjection, ReadStateScope, ReadStateSummaryProjection, RecoverySummaryProjection, RunResultProjection, ScratchpadProjection, TaskContractProjection, TaskDetailProjection, TaskDetailResultProjection, TaskProjection, WorklistProjection } from "@tutti/shared/schemas/api";
5
5
  export type MessageAuthorInput = {
6
6
  kind: "human" | "system" | "agent";
@@ -53,9 +53,15 @@ export type ScratchpadSnapshotInput = {
53
53
  current_consensus: string[];
54
54
  open_questions: string[];
55
55
  task_changes: string[];
56
+ source_refs?: ScratchpadSourceRefInput[];
56
57
  updated_by_activity_ref?: ActivityRef;
57
58
  now?: () => Date;
58
59
  };
60
+ export type ScratchpadSourceRefInput = {
61
+ message_id: MessageId;
62
+ source_scope: "main_chat" | "clarification_round";
63
+ created_at: string;
64
+ };
59
65
  export type SubmitScratchpadInput = {
60
66
  workflow_ref: WorkflowInvocationRef;
61
67
  activity_ref: ActivityRef;
@@ -286,6 +292,7 @@ export type ReadMessageWindowOptions = {
286
292
  anchor?: "latest" | "start";
287
293
  before?: string;
288
294
  after?: string;
295
+ around?: string;
289
296
  };
290
297
  export type ReadMainChatMessagesAfterOptions = {
291
298
  after_created_at: string;
@@ -1,6 +1,7 @@
1
1
  import { createWorkflowInvocationRef, type ActivityRef } from "@tutti/shared/ids";
2
2
  import type { RefreshScratchpadDisposition, RefreshScratchpadResult, ProcedureLane } from "@tutti/shared/schemas/api";
3
3
  import type { ScratchpadSourceCursorUpdate } from "../collaboration-state/scratchpad-source-state.js";
4
+ import type { ScratchpadSourceRefInput } from "../collaboration-state/types.js";
4
5
  import type { ProcedureEngine, ProcedureEngineState } from "../procedure-engine/index.js";
5
6
  import type { HostProjectStore } from "../store/index.js";
6
7
  import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
@@ -12,6 +13,7 @@ type ScratchpadProjectContext = {
12
13
  export type ScratchpadSourceBatch = {
13
14
  workflow_input: Parameters<ProcedureWorkflowRunner["runScratchpadRefresh"]>[0];
14
15
  cursor_update: ScratchpadSourceCursorUpdate;
16
+ source_refs: ScratchpadSourceRefInput[];
15
17
  source_message_count: number;
16
18
  };
17
19
  export declare function readControlPlaneScratchpadSourceBatch(store: HostProjectStore, options?: {
@@ -96,6 +96,10 @@ export function readControlPlaneScratchpadSourceBatch(store, options = {}) {
96
96
  ...newMainChatSourceMessages,
97
97
  ...clarificationSourceMessages,
98
98
  ]);
99
+ const sourceRefMessages = sortScratchpadSourceMessages([
100
+ ...(isInitialMainChatRefresh ? recentMessages : []),
101
+ ...sourceMessages,
102
+ ]);
99
103
  const latestMainChatSource = mainChatSourceMessages.at(-1);
100
104
  const latestClarificationRound = clarificationRounds.at(-1);
101
105
  const cursorUpdate = {};
@@ -124,6 +128,11 @@ export function readControlPlaneScratchpadSourceBatch(store, options = {}) {
124
128
  new_messages: compactScratchpadSourceMessages(sourceMessages),
125
129
  },
126
130
  cursor_update: cursorUpdate,
131
+ source_refs: sourceRefMessages.map((message) => ({
132
+ message_id: message.id,
133
+ source_scope: message.scope.kind,
134
+ created_at: message.created_at,
135
+ })),
127
136
  source_message_count: sourceMessages.length + (isInitialMainChatRefresh ? recentMessages.length : 0),
128
137
  };
129
138
  }
@@ -153,6 +162,7 @@ export async function runControlPlaneScratchpadRefreshProcedure(options) {
153
162
  workflowRef: options.workflowRef,
154
163
  activityRef: options.activityRef,
155
164
  cursorUpdate: options.sourceBatch.cursor_update,
165
+ sourceRefs: options.sourceBatch.source_refs,
156
166
  now: options.now,
157
167
  });
158
168
  }
@@ -1,5 +1,6 @@
1
1
  import type { ActivityRef, WorkflowInvocationRef } from "@tutti/shared/ids";
2
2
  import type { ScratchpadSourceCursorUpdate } from "../collaboration-state/scratchpad-source-state.js";
3
+ import type { ScratchpadSourceRefInput } from "../collaboration-state/types.js";
3
4
  import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
4
5
  import type { HostProjectStore } from "../store/index.js";
5
6
  import type { ScratchpadRefreshWorkflowOutput } from "./workflows/index.js";
@@ -10,6 +11,7 @@ export declare function applyScratchpadRefreshOutput(input: {
10
11
  workflowRef: WorkflowInvocationRef;
11
12
  activityRef: ActivityRef;
12
13
  cursorUpdate: ScratchpadSourceCursorUpdate;
14
+ sourceRefs: ScratchpadSourceRefInput[];
13
15
  now: () => Date;
14
16
  }): {
15
17
  kind: "needs_human";
@@ -31,6 +31,7 @@ export function applyScratchpadRefreshOutput(input) {
31
31
  current_consensus: input.output.scratchpad_snapshot.current_consensus,
32
32
  open_questions: input.output.scratchpad_snapshot.open_questions,
33
33
  task_changes: input.output.scratchpad_snapshot.task_changes,
34
+ source_refs: input.sourceRefs,
34
35
  updated_by_activity_ref: input.activityRef,
35
36
  now: input.now,
36
37
  });
@@ -1,10 +1,24 @@
1
1
  import { type ActivityRef, type EventRef, type TaskId, type WorkflowInvocationRef } from "@tutti/shared/ids";
2
- import type { ProjectTimelineItem } from "@tutti/shared/schemas/api";
3
2
  import type { SqliteDatabase } from "../store/index.js";
4
3
  export type ProjectTimelineEventKind = "context_sync" | "reference_summary" | "promotion_reconcile";
4
+ export type ProjectTimelineEventStatus = "completed" | "failed";
5
+ export type ProjectTimelineEventItem = {
6
+ id: string;
7
+ kind: "procedure" | "promotion";
8
+ occurred_at: string;
9
+ title: string;
10
+ status: ProjectTimelineEventStatus;
11
+ summary?: string;
12
+ activity_ref?: ActivityRef;
13
+ workflow_ref?: WorkflowInvocationRef;
14
+ task_id?: TaskId;
15
+ commit_oid?: string;
16
+ commit_short_oid?: string;
17
+ meta?: string[];
18
+ };
5
19
  export type ProjectTimelineEventInput = {
6
20
  event_kind: ProjectTimelineEventKind;
7
- status: NonNullable<ProjectTimelineItem["status"]>;
21
+ status: ProjectTimelineEventStatus;
8
22
  summary?: string;
9
23
  activity_ref?: ActivityRef;
10
24
  workflow_ref?: WorkflowInvocationRef;
@@ -16,5 +30,5 @@ export type ProjectTimelineEventInput = {
16
30
  export declare function recordProjectTimelineEvent(db: SqliteDatabase, input: ProjectTimelineEventInput): EventRef;
17
31
  export declare function readProjectTimelineEventItems(db: SqliteDatabase, options?: {
18
32
  limit?: number;
19
- }): ProjectTimelineItem[];
33
+ }): ProjectTimelineEventItem[];
20
34
  //# sourceMappingURL=index.d.ts.map
@@ -10,28 +10,11 @@ import { HOST_SERVER_VERSION, startForegroundHostServer, } from "./host-server-r
10
10
  import { deleteMachineRuntimeEndpoint, readHostRegistrationSecret, readMachineProjectBinding, readMachineRuntimeEndpoint, } from "./machine-local.js";
11
11
  import { prepareLaunchProject, resolveLaunchLocalContext, } from "./launch.js";
12
12
  import { registerHostWithRelay, relayErrorToLaunchError, } from "./relay-registration.js";
13
- import { formatJoinLinkValidity } from "./terminal-qr.js";
13
+ import { formatJoinLinkValidity, formatTerminalLink } from "./terminal-qr.js";
14
14
  export { createRuntimeEndpointProbe } from "./host-runtime-endpoint.js";
15
15
  const DEFAULT_SHUTDOWN_WAIT_MS = 2_000;
16
- const OSC8_START = "\u001B]8;;";
17
- const OSC8_END = "\u001B]8;;\u001B\\";
18
- function isTerminalSafeHttpUrl(value) {
19
- if (!value.startsWith("http://") && !value.startsWith("https://")) {
20
- return false;
21
- }
22
- return Array.from(value).every((character) => {
23
- const codePoint = character.codePointAt(0);
24
- return codePoint !== undefined && codePoint > 0x1f && codePoint !== 0x7f;
25
- });
26
- }
27
- function terminalLink(value, options) {
28
- if (options.hyperlinks !== true || !isTerminalSafeHttpUrl(value)) {
29
- return value;
30
- }
31
- return `${OSC8_START}${value}\u001B\\${value}${OSC8_END}`;
32
- }
33
16
  function urlLine(label, value, options) {
34
- return `${label}: ${terminalLink(value, options)}`;
17
+ return `${label}: ${formatTerminalLink(value, options)}`;
35
18
  }
36
19
  function endpointWithoutToken(endpoint) {
37
20
  return {
@@ -1,5 +1,16 @@
1
1
  import type { LaunchConfirmationRequest } from "./git-bootstrap.js";
2
2
  export declare function confirmFromTty(request: LaunchConfirmationRequest): Promise<boolean>;
3
+ export declare function renderCompletionPage(options: {
4
+ joinUrl: string;
5
+ joinTokenExpiresAt?: string;
6
+ joinTokenReusable?: boolean;
7
+ projectId: string;
8
+ workspaceRoot: string;
9
+ relayUrl: string;
10
+ branch?: string;
11
+ tty?: boolean;
12
+ hyperlinks?: boolean;
13
+ }): string;
3
14
  export declare function runForegroundLaunchCommand(options: {
4
15
  target?: string;
5
16
  yes: boolean;
@@ -1,9 +1,10 @@
1
1
  import { createInterface } from "node:readline/promises";
2
+ import { basename } from "node:path";
2
3
  import { formatLaunchLifecycleResult, startLaunchProject, waitForForegroundHostShutdown } from "./host-lifecycle.js";
3
4
  import { prepareLaunchProject } from "./launch.js";
4
5
  import { spawnDetachedHost, waitForManagedHostReady } from "./managed-host.js";
5
6
  import { resolveLaunchWorkspacePath } from "./project-resolver.js";
6
- import { formatJoinLinkValidity, renderTerminalQr } from "./terminal-qr.js";
7
+ import { formatJoinLinkValidity, formatTerminalLink, renderTerminalQr } from "./terminal-qr.js";
7
8
  import { renderTuttiTerminalLogo } from "./terminal-logo.js";
8
9
  import { runProviderSetupTui } from "./provider-tui.js";
9
10
  import { LaunchError } from "./errors.js";
@@ -39,13 +40,39 @@ async function waitForEnter() {
39
40
  readline.close();
40
41
  }
41
42
  }
42
- function renderCompletionPage(options) {
43
- const isTty = process.stdout.isTTY === true;
43
+ function formatProjectLabel(workspaceRoot, projectId) {
44
+ return basename(workspaceRoot) || projectId;
45
+ }
46
+ function borderedBlock(lines) {
47
+ const width = Math.max(...lines.map((line) => line.length));
48
+ const top = `╭${"─".repeat(width + 2)}╮`;
49
+ const bottom = `╰${"─".repeat(width + 2)}╯`;
50
+ const body = lines.map((line) => `│ ${line.padEnd(width, " ")} │`);
51
+ return [top, ...body, bottom].join("\n");
52
+ }
53
+ export function renderCompletionPage(options) {
54
+ const isTty = options.tty ?? process.stdout.isTTY === true;
55
+ const projectLabel = formatProjectLabel(options.workspaceRoot, options.projectId);
56
+ const joinUrl = formatTerminalLink(options.joinUrl, { hyperlinks: options.hyperlinks === true });
57
+ const summaryLines = [
58
+ "Tutti is hosting this project.",
59
+ `Project: ${projectLabel}`,
60
+ "Status: background host ready",
61
+ `Branch: ${options.branch ?? "tutti/mainline"}`,
62
+ `Relay: ${options.relayUrl}`,
63
+ ];
64
+ const nextSteps = [
65
+ "Next:",
66
+ " Open the join URL or scan the QR code.",
67
+ " `tutti invite` rotates a fresh link.",
68
+ " `tutti ps` shows running hosts.",
69
+ " `tutti stop` stops this host.",
70
+ ];
44
71
  return [
45
72
  ...(isTty ? [CLEAR, renderTuttiTerminalLogo({ tty: true }), ""] : []),
46
- "Tutti is running in the background.",
73
+ isTty ? borderedBlock(summaryLines) : summaryLines.join("\n"),
47
74
  "",
48
- `Join URL: ${options.joinUrl}`,
75
+ `Join URL: ${joinUrl}`,
49
76
  formatJoinLinkValidity({
50
77
  ...(options.joinTokenExpiresAt === undefined
51
78
  ? {}
@@ -55,9 +82,12 @@ function renderCompletionPage(options) {
55
82
  : { reusable: options.joinTokenReusable }),
56
83
  }),
57
84
  "",
85
+ "QR code:",
58
86
  renderTerminalQr(options.joinUrl),
59
87
  "",
60
- "[Enter] to continue",
88
+ ...nextSteps,
89
+ "",
90
+ isTty ? "[Enter] close this view. The host keeps running." : "The host keeps running.",
61
91
  ].join("\n");
62
92
  }
63
93
  async function ensureProviderConfigured(preparation) {
@@ -118,6 +148,12 @@ export async function runBackgroundLaunchCommand(options) {
118
148
  }
119
149
  process.stdout.write(`${renderCompletionPage({
120
150
  joinUrl: ready.join_url,
151
+ projectId: preparation.project_id,
152
+ workspaceRoot: preparation.workspace_root,
153
+ relayUrl: preparation.relay_url,
154
+ branch: preparation.branch,
155
+ tty: process.stdout.isTTY === true,
156
+ hyperlinks: process.stdout.isTTY === true,
121
157
  ...(ready.join_token_expires_at === undefined
122
158
  ? {}
123
159
  : { joinTokenExpiresAt: ready.join_token_expires_at }),
@@ -1,3 +1,6 @@
1
+ export declare function formatTerminalLink(value: string, options?: {
2
+ hyperlinks?: boolean;
3
+ }): string;
1
4
  export declare function renderTerminalQr(input: string): string;
2
5
  export declare function formatJoinLinkValidity(options: {
3
6
  expiresAt?: string;
@@ -1,5 +1,22 @@
1
1
  import { createRequire } from "node:module";
2
2
  const require = createRequire(import.meta.url);
3
+ const OSC8_START = "\u001B]8;;";
4
+ const OSC8_END = "\u001B]8;;\u001B\\";
5
+ function isTerminalSafeHttpUrl(value) {
6
+ if (!value.startsWith("http://") && !value.startsWith("https://")) {
7
+ return false;
8
+ }
9
+ return Array.from(value).every((character) => {
10
+ const codePoint = character.codePointAt(0);
11
+ return codePoint !== undefined && codePoint > 0x1f && codePoint !== 0x7f;
12
+ });
13
+ }
14
+ export function formatTerminalLink(value, options = {}) {
15
+ if (options.hyperlinks !== true || !isTerminalSafeHttpUrl(value)) {
16
+ return value;
17
+ }
18
+ return `${OSC8_START}${value}\u001B\\${value}${OSC8_END}`;
19
+ }
3
20
  export function renderTerminalQr(input) {
4
21
  const qrcode = require("qrcode-terminal");
5
22
  let output = "";
@@ -8,6 +8,7 @@ export declare const HOST_PROJECT_COLLABORATION_OPENAPI_ROUTES: ({
8
8
  anchor: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"latest">, import("@sinclair/typebox").TLiteral<"start">]>>;
9
9
  before: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
10
10
  after: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
11
+ around: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
11
12
  }>;
12
13
  responses: {
13
14
  200: {
@@ -67,7 +67,7 @@ export declare const HOST_PROJECT_TIMELINE_OPENAPI_ROUTES: {
67
67
  }>;
68
68
  items: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
69
69
  id: import("@sinclair/typebox").TString;
70
- kind: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"run">, import("@sinclair/typebox").TLiteral<"procedure">, import("@sinclair/typebox").TLiteral<"commit">, import("@sinclair/typebox").TLiteral<"usage">, import("@sinclair/typebox").TLiteral<"clarification">, import("@sinclair/typebox").TLiteral<"checks">, import("@sinclair/typebox").TLiteral<"promotion">, import("@sinclair/typebox").TLiteral<"recovery">]>;
70
+ kind: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"task_accepted">, import("@sinclair/typebox").TLiteral<"task_update">, import("@sinclair/typebox").TLiteral<"checkpoint">]>;
71
71
  occurred_at: import("@sinclair/typebox").TString;
72
72
  title: import("@sinclair/typebox").TString;
73
73
  summary: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
@@ -75,9 +75,10 @@ export declare const HOST_PROJECT_TIMELINE_OPENAPI_ROUTES: {
75
75
  task_title: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
76
76
  activity_ref: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
77
77
  workflow_ref: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
78
- commit_oid: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
79
- commit_short_oid: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
80
- status: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"running">, import("@sinclair/typebox").TLiteral<"completed">, import("@sinclair/typebox").TLiteral<"failed">, import("@sinclair/typebox").TLiteral<"needs_human">, import("@sinclair/typebox").TLiteral<"skipped">]>>;
78
+ run_result_id: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
79
+ message_id: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
80
+ status: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"completed">, import("@sinclair/typebox").TLiteral<"failed">, import("@sinclair/typebox").TLiteral<"needs_human">]>>;
81
+ update_state: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"needs_input">, import("@sinclair/typebox").TLiteral<"completed">, import("@sinclair/typebox").TLiteral<"failed">]>>;
81
82
  token_usage: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
82
83
  input_tokens: import("@sinclair/typebox").TInteger;
83
84
  cached_input_tokens: import("@sinclair/typebox").TInteger;
@@ -106,7 +107,95 @@ export declare const HOST_PROJECT_TIMELINE_OPENAPI_ROUTES: {
106
107
  total_tokens: import("@sinclair/typebox").TInteger;
107
108
  event_count: import("@sinclair/typebox").TInteger;
108
109
  }>>>;
109
- meta: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
110
+ trace_card: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
111
+ tone: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"neutral">, import("@sinclair/typebox").TLiteral<"success">, import("@sinclair/typebox").TLiteral<"warning">, import("@sinclair/typebox").TLiteral<"error">]>;
112
+ summary: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
113
+ origin: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
114
+ title: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
115
+ summary: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
116
+ snippets: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
117
+ message_id: import("@sinclair/typebox").TString;
118
+ author: import("@sinclair/typebox").TString;
119
+ body: import("@sinclair/typebox").TString;
120
+ created_at: import("@sinclair/typebox").TString;
121
+ }>>>;
122
+ source_count: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
123
+ unavailable_reason: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
124
+ }>>;
125
+ decisions: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
126
+ title: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
127
+ summary: import("@sinclair/typebox").TString;
128
+ source: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"scratchpad">, import("@sinclair/typebox").TLiteral<"clarification">, import("@sinclair/typebox").TLiteral<"contract">]>>;
129
+ }>>>;
130
+ clarification: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
131
+ title: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
132
+ summary: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
133
+ request: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
134
+ status: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"active">, import("@sinclair/typebox").TLiteral<"resolved">]>>;
135
+ answer_count: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
136
+ answers: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
137
+ message_id: import("@sinclair/typebox").TString;
138
+ author: import("@sinclair/typebox").TString;
139
+ body: import("@sinclair/typebox").TString;
140
+ created_at: import("@sinclair/typebox").TString;
141
+ }>>>;
142
+ }>>;
143
+ contract: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
144
+ goal: import("@sinclair/typebox").TString;
145
+ scope: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>;
146
+ truncated: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
147
+ }>>;
148
+ result: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
149
+ summary: import("@sinclair/typebox").TString;
150
+ error_code: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
151
+ }>>;
152
+ changes: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
153
+ items: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
154
+ status: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"added">, import("@sinclair/typebox").TLiteral<"modified">, import("@sinclair/typebox").TLiteral<"deleted">, import("@sinclair/typebox").TLiteral<"renamed">, import("@sinclair/typebox").TLiteral<"unknown">]>;
155
+ path: import("@sinclair/typebox").TString;
156
+ previous_path: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
157
+ }>>;
158
+ total_count: import("@sinclair/typebox").TInteger;
159
+ truncated: import("@sinclair/typebox").TBoolean;
160
+ }>>;
161
+ verification: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
162
+ checks: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"passed">, import("@sinclair/typebox").TLiteral<"failed">, import("@sinclair/typebox").TLiteral<"skipped">]>;
163
+ skip_reason_code: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
164
+ skip_reason_summary: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
165
+ }>>;
166
+ promotion: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
167
+ status: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"promoted">, import("@sinclair/typebox").TLiteral<"not_promoted">, import("@sinclair/typebox").TLiteral<"not_attempted">]>;
168
+ commit_oid: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
169
+ commit_short_oid: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
170
+ summary: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
171
+ reason_code: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
172
+ }>>;
173
+ checkpoint: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
174
+ counters: import("@sinclair/typebox").TObject<{
175
+ completed: import("@sinclair/typebox").TInteger;
176
+ failed: import("@sinclair/typebox").TInteger;
177
+ remaining: import("@sinclair/typebox").TInteger;
178
+ }>;
179
+ items: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
180
+ task_id: import("@sinclair/typebox").TString;
181
+ title: import("@sinclair/typebox").TString;
182
+ status: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"done">, import("@sinclair/typebox").TLiteral<"failed">]>;
183
+ summary: import("@sinclair/typebox").TString;
184
+ }>>;
185
+ generated_files: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
186
+ path: import("@sinclair/typebox").TString;
187
+ name: import("@sinclair/typebox").TString;
188
+ }>>>;
189
+ truncated: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
190
+ }>>;
191
+ links: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
192
+ kind: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"worklist_task">, import("@sinclair/typebox").TLiteral<"chat_message">, import("@sinclair/typebox").TLiteral<"reference_file">, import("@sinclair/typebox").TLiteral<"repo_path">]>;
193
+ label: import("@sinclair/typebox").TString;
194
+ task_id: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
195
+ message_id: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
196
+ path: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
197
+ }>>;
198
+ }>>;
110
199
  }>>;
111
200
  }>;
112
201
  };