@pellux/goodvibes-agent 1.5.5 → 1.5.6

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 (165) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +1 -1
  3. package/dist/package/main.js +94130 -79335
  4. package/docs/tools-and-commands.md +2 -1
  5. package/package.json +6 -3
  6. package/src/agent/autonomy-schedule-format.ts +1 -1
  7. package/src/agent/autonomy-schedule.ts +3 -3
  8. package/src/agent/calendar/calendar-oauth-service.ts +324 -0
  9. package/src/agent/calendar-subscription-registry.ts +497 -0
  10. package/src/agent/email/email-service.ts +57 -0
  11. package/src/agent/email/smtp-client.ts +30 -10
  12. package/src/agent/memory-prompt.ts +143 -12
  13. package/src/agent/operator-actions.ts +26 -22
  14. package/src/agent/prompt-context-receipts.ts +45 -18
  15. package/src/agent/reminder-schedule-format.ts +1 -1
  16. package/src/agent/reminder-schedule.ts +3 -3
  17. package/src/agent/routine-schedule-format.ts +2 -2
  18. package/src/agent/routine-schedule-promotion.ts +5 -5
  19. package/src/agent/routine-schedule-receipts.ts +2 -2
  20. package/src/agent/schedule-edit-format.ts +1 -1
  21. package/src/agent/schedule-edit.ts +5 -5
  22. package/src/agent/session-registration.ts +281 -0
  23. package/src/agent/vibe-file.ts +50 -0
  24. package/src/cli/local-library-command-shared.ts +146 -0
  25. package/src/cli/management.ts +2 -1
  26. package/src/cli/personas-command.ts +252 -0
  27. package/src/cli/resume-relaunch-notice.ts +151 -0
  28. package/src/cli/routines-command.ts +25 -2
  29. package/src/cli/service-posture.ts +3 -2
  30. package/src/cli/skill-bundle-command.ts +175 -0
  31. package/src/cli/skills-command.ts +309 -0
  32. package/src/cli/status.ts +43 -7
  33. package/src/cli/tui-startup.ts +24 -1
  34. package/src/config/agent-settings-policy.ts +0 -1
  35. package/src/config/secret-config.ts +4 -0
  36. package/src/core/conversation-rendering.ts +20 -11
  37. package/src/core/focus-tracker.ts +41 -0
  38. package/src/core/system-message-noise.ts +108 -0
  39. package/src/core/system-message-router.ts +54 -1
  40. package/src/core/thinking-overlay.ts +83 -0
  41. package/src/input/agent-workspace-access-command-editor-submission.ts +75 -108
  42. package/src/input/agent-workspace-access-command-editors.ts +108 -128
  43. package/src/input/agent-workspace-activation.ts +15 -0
  44. package/src/input/agent-workspace-basic-command-editor-submission.ts +292 -513
  45. package/src/input/agent-workspace-basic-command-editors.ts +433 -564
  46. package/src/input/agent-workspace-calendar-connect-editor.ts +116 -0
  47. package/src/input/agent-workspace-calendar-oauth-editor.ts +152 -0
  48. package/src/input/agent-workspace-calendar-subscribe-editor.ts +128 -0
  49. package/src/input/agent-workspace-categories.ts +7 -2
  50. package/src/input/agent-workspace-channel-command-editor-submission.ts +38 -52
  51. package/src/input/agent-workspace-channel-command-editors.ts +42 -46
  52. package/src/input/agent-workspace-command-editor-engine.ts +133 -0
  53. package/src/input/agent-workspace-command-editor.ts +4 -0
  54. package/src/input/agent-workspace-direct-editor-submission.ts +59 -0
  55. package/src/input/agent-workspace-email-connect-editor.ts +162 -0
  56. package/src/input/agent-workspace-knowledge-command-editor-submission.ts +52 -81
  57. package/src/input/agent-workspace-knowledge-command-editors.ts +71 -71
  58. package/src/input/agent-workspace-library-command-editor-submission.ts +10 -28
  59. package/src/input/agent-workspace-library-command-editors.ts +16 -2
  60. package/src/input/agent-workspace-live-counters.ts +55 -0
  61. package/src/input/agent-workspace-mcp-command-editor-submission.ts +60 -82
  62. package/src/input/agent-workspace-mcp-command-editors.ts +52 -0
  63. package/src/input/agent-workspace-media-command-editor-submission.ts +12 -39
  64. package/src/input/agent-workspace-media-command-editors.ts +10 -8
  65. package/src/input/agent-workspace-memory-command-editor-submission.ts +76 -151
  66. package/src/input/agent-workspace-memory-command-editors.ts +116 -141
  67. package/src/input/agent-workspace-operations-command-editor-submission.ts +131 -184
  68. package/src/input/agent-workspace-operations-command-editors.ts +150 -162
  69. package/src/input/agent-workspace-provider-command-editor-submission.ts +60 -106
  70. package/src/input/agent-workspace-provider-command-editors.ts +58 -68
  71. package/src/input/agent-workspace-search.ts +8 -1
  72. package/src/input/agent-workspace-session-command-editor-submission.ts +104 -132
  73. package/src/input/agent-workspace-session-command-editors.ts +160 -195
  74. package/src/input/agent-workspace-settings.ts +40 -1
  75. package/src/input/agent-workspace-skill-bundle-command-editor-submission.ts +55 -71
  76. package/src/input/agent-workspace-skill-bundle-command-editors.ts +65 -69
  77. package/src/input/agent-workspace-snapshot-builders.ts +431 -0
  78. package/src/input/agent-workspace-snapshot-config.ts +43 -0
  79. package/src/input/agent-workspace-snapshot.ts +198 -432
  80. package/src/input/agent-workspace-task-command-editor-submission.ts +34 -44
  81. package/src/input/agent-workspace-task-command-editors.ts +27 -28
  82. package/src/input/agent-workspace-types.ts +35 -1
  83. package/src/input/agent-workspace.ts +5 -14
  84. package/src/input/command-registry.ts +23 -1
  85. package/src/input/commands/calendar-connect-runtime.ts +226 -0
  86. package/src/input/commands/calendar-runtime.ts +110 -7
  87. package/src/input/commands/calendar-subscription-runtime.ts +225 -0
  88. package/src/input/commands/email-runtime.ts +100 -40
  89. package/src/input/commands/knowledge.ts +1 -1
  90. package/src/input/commands/network-scan-runtime.ts +75 -0
  91. package/src/input/commands/operator-actions-runtime.ts +9 -6
  92. package/src/input/commands/personas-runtime.ts +1 -1
  93. package/src/input/commands/schedule-runtime.ts +4 -4
  94. package/src/input/commands/session-content.ts +13 -1
  95. package/src/input/commands/session-workflow.ts +15 -20
  96. package/src/input/commands/session.ts +3 -1
  97. package/src/input/commands/shell-core.ts +20 -6
  98. package/src/input/commands.ts +2 -2
  99. package/src/input/delete-key-policy.ts +46 -0
  100. package/src/input/feed-context-factory.ts +10 -0
  101. package/src/input/handler-feed.ts +87 -0
  102. package/src/input/handler-modal-routes.ts +6 -1
  103. package/src/input/handler.ts +5 -0
  104. package/src/input/panel-paste-flood-guard.ts +94 -0
  105. package/src/input/settings-modal-types.ts +5 -3
  106. package/src/input/settings-modal.ts +21 -0
  107. package/src/main.ts +36 -36
  108. package/src/permissions/approval-posture.ts +141 -0
  109. package/src/renderer/agent-workspace-context-lines.ts +10 -2
  110. package/src/renderer/compositor.ts +27 -4
  111. package/src/renderer/diff.ts +61 -18
  112. package/src/renderer/fullscreen-primitives.ts +37 -18
  113. package/src/renderer/markdown.ts +20 -10
  114. package/src/renderer/modal-factory.ts +25 -15
  115. package/src/renderer/overlay-box.ts +23 -12
  116. package/src/renderer/process-indicator.ts +8 -3
  117. package/src/renderer/prompt-content-width.ts +16 -0
  118. package/src/renderer/settings-modal-helpers.ts +2 -0
  119. package/src/renderer/settings-modal.ts +2 -0
  120. package/src/renderer/startup-theme-probe.ts +35 -0
  121. package/src/renderer/status-glyphs.ts +11 -15
  122. package/src/renderer/system-message.ts +17 -2
  123. package/src/renderer/term-caps.ts +318 -0
  124. package/src/renderer/terminal-bg-probe.ts +373 -0
  125. package/src/renderer/terminal-escapes.ts +24 -0
  126. package/src/renderer/theme-mode-config.ts +87 -0
  127. package/src/renderer/theme.ts +241 -0
  128. package/src/renderer/thinking.ts +12 -3
  129. package/src/renderer/tool-call.ts +7 -3
  130. package/src/renderer/ui-factory.ts +92 -36
  131. package/src/renderer/ui-primitives.ts +33 -93
  132. package/src/runtime/bootstrap-core.ts +15 -0
  133. package/src/runtime/bootstrap-hook-bridge.ts +6 -0
  134. package/src/runtime/bootstrap-shell.ts +2 -0
  135. package/src/runtime/bootstrap.ts +68 -5
  136. package/src/runtime/calendar-boot-refresh.ts +105 -0
  137. package/src/runtime/lan-scan-consent.ts +253 -0
  138. package/src/runtime/services.ts +127 -2
  139. package/src/runtime/session-spine-rest-transport.ts +160 -0
  140. package/src/runtime/terminal-output-guard.ts +6 -1
  141. package/src/runtime/ui-services.ts +3 -0
  142. package/src/shell/agent-workspace-fullscreen.ts +5 -0
  143. package/src/shell/terminal-focus-mode.ts +120 -0
  144. package/src/shell/ui-openers.ts +13 -4
  145. package/src/tools/agent-harness-autonomy-intake.ts +5 -5
  146. package/src/tools/agent-harness-autonomy-queue.ts +1 -1
  147. package/src/tools/agent-harness-background-processes.ts +2 -1
  148. package/src/tools/agent-harness-metadata.ts +16 -8
  149. package/src/tools/agent-harness-operator-methods.ts +44 -8
  150. package/src/tools/agent-harness-personal-ops-discovery.ts +57 -10
  151. package/src/tools/agent-harness-personal-ops-lanes.ts +17 -5
  152. package/src/tools/agent-harness-personal-ops-operations.ts +7 -7
  153. package/src/tools/agent-harness-personal-ops-types.ts +6 -0
  154. package/src/tools/agent-harness-prompt-context.ts +26 -12
  155. package/src/tools/agent-harness-workspace-actions.ts +4 -0
  156. package/src/tools/agent-local-registry-args.ts +117 -0
  157. package/src/tools/agent-local-registry-memory.ts +227 -0
  158. package/src/tools/agent-local-registry-tool.ts +19 -237
  159. package/src/tools/agent-operator-briefing-tool.ts +2 -2
  160. package/src/tools/agent-operator-method-tool.ts +13 -0
  161. package/src/tools/agent-policy-explanation.ts +39 -4
  162. package/src/tools/agent-schedule-tool.ts +5 -5
  163. package/src/utils/terminal-width.ts +98 -1
  164. package/src/version.ts +1 -1
  165. package/src/cli/local-library-command.ts +0 -825
@@ -1,61 +1,51 @@
1
- import type { AgentWorkspaceActionResult, AgentWorkspaceEditorKind, AgentWorkspaceLocalEditor } from './agent-workspace-types.ts';
1
+ import type { AgentWorkspaceEditorKind, AgentWorkspaceLocalEditor } from './agent-workspace-types.ts';
2
2
  import type { AgentWorkspaceTaskCommandEditorKind } from './agent-workspace-task-command-editors.ts';
3
3
  import { isAgentWorkspaceTaskCommandEditorKind } from './agent-workspace-task-command-editors.ts';
4
4
  import { quoteSlashCommandArg } from './slash-command-parser.ts';
5
+ import type { AgentWorkspaceCommandEditorSubmission, AgentWorkspaceCommandSubmissionHandler, AgentWorkspaceFieldReader } from './agent-workspace-command-editor-engine.ts';
6
+ import { buildCommandEditorSubmissionFromTable, dispatchCommandEditorSubmission } from './agent-workspace-command-editor-engine.ts';
5
7
 
6
- type AgentWorkspaceFieldReader = (fieldId: string) => string;
7
-
8
- export type AgentWorkspaceTaskCommandEditorSubmission =
9
- | {
10
- readonly kind: 'editor';
11
- readonly editor: AgentWorkspaceLocalEditor;
12
- readonly status: string;
13
- readonly actionResult?: AgentWorkspaceActionResult;
14
- }
15
- | {
16
- readonly kind: 'dispatch';
17
- readonly command: string;
18
- readonly status: string;
19
- readonly actionResult: AgentWorkspaceActionResult;
20
- };
8
+ export type AgentWorkspaceTaskCommandEditorSubmission = AgentWorkspaceCommandEditorSubmission;
21
9
 
22
10
  export function isAgentWorkspaceTaskCommandSubmissionKind(kind: AgentWorkspaceEditorKind): kind is AgentWorkspaceTaskCommandEditorKind {
23
11
  return isAgentWorkspaceTaskCommandEditorKind(kind);
24
12
  }
25
13
 
26
- export function buildAgentWorkspaceTaskCommandEditorSubmission(
27
- editor: AgentWorkspaceLocalEditor,
28
- readField: AgentWorkspaceFieldReader,
29
- ): AgentWorkspaceTaskCommandEditorSubmission {
30
- if (editor.kind === 'task-list-filter') {
14
+ const TASK_COMMAND_SUBMISSION_HANDLERS: Readonly<Record<AgentWorkspaceTaskCommandEditorKind, AgentWorkspaceCommandSubmissionHandler>> = {
15
+ 'task-list-filter': (_editor, readField) => {
31
16
  const filter = readField('filter');
32
17
  const command = filter.length > 0 ? `/tasks list ${quoteSlashCommandArg(filter)}` : '/tasks list';
33
- return {
34
- kind: 'dispatch',
18
+ return dispatchCommandEditorSubmission(
35
19
  command,
36
- status: 'Opening filtered task list.',
37
- actionResult: {
38
- kind: 'dispatched',
39
- title: 'Opening filtered task list',
40
- detail: 'The workspace handed read-only connected-host task listing to the shell-owned command router.',
41
- command,
42
- safety: 'read-only',
43
- },
44
- };
45
- }
20
+ 'Opening filtered task list',
21
+ 'The workspace handed read-only connected-host task listing to the shell-owned command router.',
22
+ 'read-only',
23
+ );
24
+ },
25
+ 'task-output': (editor, readField) => taskInspection(editor, readField),
26
+ 'task-show': (editor, readField) => taskInspection(editor, readField),
27
+ };
28
+
29
+ function taskInspection(editor: AgentWorkspaceLocalEditor, readField: AgentWorkspaceFieldReader): AgentWorkspaceCommandEditorSubmission {
46
30
  const subcommand = editor.kind === 'task-output' ? 'output' : 'show';
47
31
  const command = `/tasks ${subcommand} ${quoteSlashCommandArg(readField('taskId'))}`;
48
32
  const title = editor.kind === 'task-output' ? 'Opening task output' : 'Opening task inspection';
49
- return {
50
- kind: 'dispatch',
33
+ return dispatchCommandEditorSubmission(
51
34
  command,
52
- status: `${title}.`,
53
- actionResult: {
54
- kind: 'dispatched',
55
- title,
56
- detail: 'The workspace handed read-only connected-host task inspection to the shell-owned command router.',
57
- command,
58
- safety: 'read-only',
59
- },
60
- };
35
+ title,
36
+ 'The workspace handed read-only connected-host task inspection to the shell-owned command router.',
37
+ 'read-only',
38
+ );
39
+ }
40
+
41
+ export function buildAgentWorkspaceTaskCommandEditorSubmission(
42
+ editor: AgentWorkspaceLocalEditor,
43
+ readField: AgentWorkspaceFieldReader,
44
+ ): AgentWorkspaceCommandEditorSubmission {
45
+ return buildCommandEditorSubmissionFromTable(
46
+ editor.kind as AgentWorkspaceTaskCommandEditorKind,
47
+ editor,
48
+ readField,
49
+ TASK_COMMAND_SUBMISSION_HANDLERS,
50
+ );
61
51
  }
@@ -1,4 +1,6 @@
1
1
  import type { AgentWorkspaceEditorKind, AgentWorkspaceLocalEditor } from './agent-workspace-types.ts';
2
+ import type { AgentWorkspaceEditorSpecEntry } from './agent-workspace-command-editor-engine.ts';
3
+ import { createAgentWorkspaceEditorFromTable } from './agent-workspace-command-editor-engine.ts';
2
4
 
3
5
  export type AgentWorkspaceTaskCommandEditorKind = Extract<
4
6
  AgentWorkspaceEditorKind,
@@ -9,33 +11,26 @@ export function isAgentWorkspaceTaskCommandEditorKind(kind: AgentWorkspaceEditor
9
11
  return kind === 'task-list-filter' || kind === 'task-show' || kind === 'task-output';
10
12
  }
11
13
 
12
- export function createAgentWorkspaceTaskCommandEditor(kind: AgentWorkspaceTaskCommandEditorKind): AgentWorkspaceLocalEditor {
13
- if (kind === 'task-list-filter') {
14
- return {
15
- kind,
16
- mode: 'create',
17
- title: 'Filter Host Tasks',
18
- selectedFieldIndex: 0,
19
- message: 'List connected-host tasks filtered by status or kind. This is read-only and never creates, retries, or cancels tasks.',
20
- fields: [
21
- { id: 'filter', label: 'Status or kind', value: '', required: false, multiline: false, hint: 'Optional task status or kind. Blank lists recent tasks.' },
22
- ],
23
- };
24
- }
25
- if (kind === 'task-output') {
26
- return {
27
- kind,
28
- mode: 'create',
29
- title: 'Show Task Output',
30
- selectedFieldIndex: 0,
31
- message: 'Print one connected-host task output from the Agent workspace. This is read-only and does not retry, cancel, or mutate the task.',
32
- fields: [
33
- { id: 'taskId', label: 'Task id', value: '', required: true, multiline: false, hint: 'Connected-host task id from Host tasks.' },
34
- ],
35
- };
36
- }
37
- return {
38
- kind,
14
+ const TASK_COMMAND_EDITOR_SPECS: Readonly<Record<AgentWorkspaceTaskCommandEditorKind, AgentWorkspaceEditorSpecEntry<AgentWorkspaceTaskCommandEditorKind>>> = {
15
+ 'task-list-filter': {
16
+ mode: 'create',
17
+ title: 'Filter Host Tasks',
18
+ selectedFieldIndex: 0,
19
+ message: 'List connected-host tasks filtered by status or kind. This is read-only and never creates, retries, or cancels tasks.',
20
+ fields: [
21
+ { id: 'filter', label: 'Status or kind', value: '', required: false, multiline: false, hint: 'Optional task status or kind. Blank lists recent tasks.' },
22
+ ],
23
+ },
24
+ 'task-output': {
25
+ mode: 'create',
26
+ title: 'Show Task Output',
27
+ selectedFieldIndex: 0,
28
+ message: 'Print one connected-host task output from the Agent workspace. This is read-only and does not retry, cancel, or mutate the task.',
29
+ fields: [
30
+ { id: 'taskId', label: 'Task id', value: '', required: true, multiline: false, hint: 'Connected-host task id from Host tasks.' },
31
+ ],
32
+ },
33
+ 'task-show': {
39
34
  mode: 'create',
40
35
  title: 'Inspect Host Task',
41
36
  selectedFieldIndex: 0,
@@ -43,5 +38,9 @@ export function createAgentWorkspaceTaskCommandEditor(kind: AgentWorkspaceTaskCo
43
38
  fields: [
44
39
  { id: 'taskId', label: 'Task id', value: '', required: true, multiline: false, hint: 'Connected-host task id from Host tasks.' },
45
40
  ],
46
- };
41
+ },
42
+ };
43
+
44
+ export function createAgentWorkspaceTaskCommandEditor(kind: AgentWorkspaceTaskCommandEditorKind): AgentWorkspaceLocalEditor {
45
+ return createAgentWorkspaceEditorFromTable(kind, TASK_COMMAND_EDITOR_SPECS);
47
46
  }
@@ -241,7 +241,12 @@ export type AgentWorkspaceEditorKind =
241
241
  | 'workplan-delete'
242
242
  | 'workplan-clear-completed'
243
243
  | 'routine-schedule'
244
- | 'reminder-schedule';
244
+ | 'reminder-schedule'
245
+ | 'email-connect-wizard'
246
+ | 'calendar-connect'
247
+ | 'calendar-subscribe-wizard'
248
+ | 'calendar-oauth-google'
249
+ | 'calendar-oauth-outlook';
245
250
 
246
251
  export type AgentWorkspaceLocalOperation =
247
252
  | 'memory-edit'
@@ -365,6 +370,13 @@ export interface AgentWorkspaceLocalLibraryItem {
365
370
  readonly missingRequirementCount?: number;
366
371
  readonly missingRequirements?: readonly string[];
367
372
  readonly startCount?: number;
373
+ /** Memory-only (Wave-4 W4-A1B): whether this record currently clears the prompt-injection
374
+ * recall floor, per describeMemoryPromptEligibility. */
375
+ readonly promptEligible?: boolean;
376
+ /** Memory-only: the honest, per-record reason from describeMemoryPromptEligibility —
377
+ * the same wording source prompt-context-receipts.ts and agent-harness-prompt-context.ts
378
+ * use, so this workspace snapshot never has to invent its own paraphrase. */
379
+ readonly promptEligibilityReason?: string;
368
380
  }
369
381
 
370
382
  export interface AgentWorkspaceRecentReviewerHandoffArtifact {
@@ -628,6 +640,14 @@ export interface AgentWorkspaceCompanionAccessSummary {
628
640
  readonly nextStep: string;
629
641
  }
630
642
 
643
+ /** Honest, no-I/O email-connect status (config validation only) for the inbox connect card/wizard (W4-A5). */
644
+ export interface AgentWorkspaceEmailConnectStatus {
645
+ readonly connected: boolean;
646
+ readonly username: string;
647
+ readonly imapHost: string;
648
+ readonly errors: readonly string[];
649
+ }
650
+
631
651
  export interface AgentWorkspaceRuntimeSnapshot {
632
652
  readonly provider: string;
633
653
  readonly model: string;
@@ -763,4 +783,18 @@ export interface AgentWorkspaceRuntimeSnapshot {
763
783
  readonly setupChecklist: readonly AgentWorkspaceSetupChecklistItem[];
764
784
  readonly setupWizard: AgentSetupWizard;
765
785
  readonly warnings: readonly string[];
786
+ /**
787
+ * True when the most recent render-path attempt to re-derive the live
788
+ * counters (memory count, routine counts + start counts — W4-A6) failed
789
+ * and the previous values below had to be kept. A fresh
790
+ * buildAgentWorkspaceRuntimeSnapshot() call is never stale (it just read
791
+ * everything); this only flips true from
792
+ * AgentWorkspace.syncLiveCountersForRender() when a live re-read errors.
793
+ * The render path uses this to label the counters honestly ("refreshing")
794
+ * instead of asserting a number the disk might already contradict.
795
+ */
796
+ readonly liveCountersStale: boolean;
797
+ /** Honest email-connect status for the connect wizard's entry state (W4-A5). Null when unavailable in this runtime. */
798
+ readonly emailConnectStatus: AgentWorkspaceEmailConnectStatus | null;
799
+ readonly calendarOAuthConfigStatus: { readonly google: boolean; readonly microsoft: boolean } | null; // F1c: per-provider client-id-configured state for the advanced cards
766
800
  }
@@ -21,7 +21,8 @@ import { appendAgentWorkspaceActionSearchText, backspaceAgentWorkspaceActionSear
21
21
  import { applyAgentWorkspaceSetupCheckpointAction } from './agent-workspace-setup-checkpoint-action.ts';
22
22
  import { agentWorkspaceSettingSchema, applyAgentWorkspaceSettingValue, buildAgentWorkspaceSettingActionDisplay, buildAgentWorkspaceSettingActionEffect, importAgentWorkspaceTuiSettings, isAgentWorkspaceActionVisible, type AgentWorkspaceSettingActionDisplay } from './agent-workspace-settings.ts';
23
23
  import { buildAgentWorkspaceRuntimeSnapshot } from './agent-workspace-snapshot.ts';
24
- import { submitAgentWorkspaceSubscriptionLoginFinishEditor, submitAgentWorkspaceSubscriptionLoginStartEditor, submitAgentWorkspaceSubscriptionLogoutEditor } from './agent-workspace-subscription-editor.ts';
24
+ import { syncAgentWorkspaceLiveCounters } from './agent-workspace-live-counters.ts';
25
+ import { trySubmitDirectHostActionEditor } from './agent-workspace-direct-editor-submission.ts';
25
26
  import type { AgentWorkspaceAction, AgentWorkspaceActionResult, AgentWorkspaceActionSearchResult, AgentWorkspaceCategory, AgentWorkspaceCategoryGroup, AgentWorkspaceCommandDispatcher, AgentWorkspaceEditorField, AgentWorkspaceFocusPane, AgentWorkspaceLocalEditor, AgentWorkspaceLocalEditorKind, AgentWorkspaceLocalLibraryItem, AgentWorkspaceLocalOperation, AgentWorkspacePromptDispatcher, AgentWorkspaceRuntimeSnapshot } from './agent-workspace-types.ts';
26
27
  import { ONBOARDING_COMPLETE_SYNTHETIC_ACTION, shouldShowOnboardingFinishFooter } from './agent-workspace-onboarding-finish.ts';
27
28
  import { completeOnboardingAction, onSubscriptionLoginSuccessAction } from './agent-workspace-onboarding-actions.ts';
@@ -218,6 +219,8 @@ export class AgentWorkspace {
218
219
  this.lastActionResult = { kind: 'refreshed', title: 'Runtime context refreshed', detail: 'Provider, model, session, local memory, runtime endpoint, and Agent knowledge route posture were re-read from the live command context.' };
219
220
  }
220
221
 
222
+ /** W4-A6 render-path live counter mirror (see syncAgentWorkspaceLiveCounters). */ syncLiveCountersForRender(): void { syncAgentWorkspaceLiveCounters({ context: this.context, runtimeSnapshot: this.runtimeSnapshot, setRuntimeSnapshot: (snapshot) => { this.runtimeSnapshot = snapshot; }, clampSelection: () => this.clampSelection() }); }
223
+
221
224
  cancelLocalEditor(): void {
222
225
  if (!this.localEditor) return;
223
226
  const title = this.localEditor.title;
@@ -554,19 +557,7 @@ export class AgentWorkspace {
554
557
  this.status = `${missing.label} is required.`;
555
558
  return;
556
559
  }
557
- if (editor.kind === 'subscription-login-start') {
558
- void submitAgentWorkspaceSubscriptionLoginStartEditor(this, editor, this.context, (id) => this.editorField(id)).finally(() => requestRender?.());
559
- return;
560
- }
561
- if (editor.kind === 'subscription-login-finish') {
562
- void submitAgentWorkspaceSubscriptionLoginFinishEditor(this, editor, this.context, (id) => this.editorField(id)).finally(() => requestRender?.());
563
- return;
564
- }
565
- if (editor.kind === 'subscription-logout') {
566
- submitAgentWorkspaceSubscriptionLogoutEditor(this, editor, this.context, (id) => this.editorField(id));
567
- requestRender?.();
568
- return;
569
- }
560
+ if (trySubmitDirectHostActionEditor(this, editor, this.context, (id) => this.editorField(id), requestRender)) return;
570
561
  if (isAgentWorkspaceCommandEditorKind(editor.kind)) {
571
562
  this.submitCommandEditor(editor);
572
563
  requestRender?.();
@@ -259,8 +259,30 @@ export class CommandRegistry {
259
259
  private commands = new Map<string, SlashCommand>();
260
260
  private aliasIndex = new Map<string, SlashCommand>();
261
261
 
262
- /** Register a command. Also indexes all aliases for O(1) lookup. */
262
+ /**
263
+ * Register a command. Also indexes all aliases for O(1) lookup.
264
+ *
265
+ * Collisions are LOUD: a second registration of the same primary name, or an
266
+ * alias that shadows an existing command's name or alias, throws instead of
267
+ * silently overwriting. Silent last-write-wins hid a real /session
268
+ * double-registration (session-workflow.ts vs commands/session.ts); the fork
269
+ * bypassed this check, so make it fail fast at startup instead.
270
+ */
263
271
  register(command: SlashCommand): void {
272
+ const existingByName = this.commands.get(command.name) ?? this.aliasIndex.get(command.name);
273
+ if (existingByName) {
274
+ throw new Error(
275
+ `Command registration collision: "${command.name}" is already registered by /${existingByName.name}.`,
276
+ );
277
+ }
278
+ for (const alias of command.aliases ?? []) {
279
+ const holder = this.commands.get(alias) ?? this.aliasIndex.get(alias);
280
+ if (holder) {
281
+ throw new Error(
282
+ `Command alias collision: "${alias}" on /${command.name} is already registered by /${holder.name}.`,
283
+ );
284
+ }
285
+ }
264
286
  this.commands.set(command.name, command);
265
287
  for (const alias of command.aliases ?? []) {
266
288
  this.aliasIndex.set(alias, command);
@@ -0,0 +1,226 @@
1
+ /**
2
+ * calendar-connect-runtime.ts — the /calendar connect|disconnect|accounts subcommands
3
+ * and the provider-event merge for /calendar upcoming (W4-A10).
4
+ *
5
+ * The OAuth network dance lives here (not in the workspace card) because a command
6
+ * handler can print the authorization URL / device user-code IMMEDIATELY, then await
7
+ * the browser callback or poll the device code. The default path uses the bundled
8
+ * project client id + PKCE; a power user's own client id/secret come from config +
9
+ * the secret manager (captured by the advanced workspace card). No secret is ever put
10
+ * into a command string.
11
+ *
12
+ * The service is injectable so tests drive every path against fakes with no real
13
+ * network, port, or browser.
14
+ */
15
+ import type { CalendarProviderId, DeviceCodeFlowStart, EventWindow, MergedCalendarEvent } from '@pellux/goodvibes-sdk/platform/calendar';
16
+ import type { CommandContext } from '../command-registry.ts';
17
+ import { requireSecretsManager } from './runtime-services.ts';
18
+ import { CalendarOAuthService, type CalendarConfigReader, type CalendarSecretSlice } from '../../agent/calendar/calendar-oauth-service.ts';
19
+
20
+ /** Factory seam: production builds the real service from context; tests inject a fake. */
21
+ export type CalendarServiceFactory = (ctx: CommandContext) => CalendarOAuthService;
22
+
23
+ const PROVIDER_LABEL: Record<CalendarProviderId, string> = { google: 'Google Calendar', microsoft: 'Microsoft Outlook' };
24
+
25
+ const CONNECTION_STATE_NOTE: Record<string, string> = {
26
+ connected: 'connected',
27
+ 'refresh-due': 'connected (token refresh due)',
28
+ 'reconnect-needed': 'reconnect needed — run the connect command again',
29
+ disconnected: 'disconnected',
30
+ };
31
+
32
+ /** Map the user's provider word to a provider id ('outlook' and 'microsoft' both ok). */
33
+ export function normalizeProvider(word: string | undefined): CalendarProviderId | null {
34
+ const w = (word ?? '').trim().toLowerCase();
35
+ if (w === 'google' || w === 'gcal') return 'google';
36
+ if (w === 'outlook' || w === 'microsoft' || w === 'ms' || w === 'msft') return 'microsoft';
37
+ return null;
38
+ }
39
+
40
+ function defaultServiceFactory(ctx: CommandContext): CalendarOAuthService {
41
+ const config = ctx.platform.configManager as unknown as CalendarConfigReader;
42
+ const secrets = requireSecretsManager(ctx) as unknown as CalendarSecretSlice;
43
+ return new CalendarOAuthService({ config, secrets });
44
+ }
45
+
46
+ function deviceCodeMessage(label: string, start: DeviceCodeFlowStart): string {
47
+ return [
48
+ `Authorize ${label} on another device or browser:`,
49
+ ` 1. Open ${start.verificationUri}`,
50
+ ` 2. Enter the code: ${start.userCode}`,
51
+ start.verificationUriComplete ? ` (or open directly: ${start.verificationUriComplete})` : '',
52
+ ' Waiting for approval...',
53
+ ].filter(Boolean).join('\n');
54
+ }
55
+
56
+ function authUrlMessage(label: string, url: string): string {
57
+ return [
58
+ `Authorize ${label} in your browser:`,
59
+ ` ${url}`,
60
+ ' (Attempting to open it automatically. Waiting for the redirect...)',
61
+ ].join('\n');
62
+ }
63
+
64
+ /**
65
+ * The real guide for a bare `/calendar connect` (F1a): what connecting does, the
66
+ * two providers, and — since this build ships only the SDK's placeholder client
67
+ * ids until someone registers a project app — the honest situation and the exact
68
+ * next step, instead of a bare usage line that dead-ends with no explanation.
69
+ */
70
+ function connectGuideMessage(): string {
71
+ return [
72
+ 'Connect a calendar account over OAuth so its events show up alongside your local ones in /calendar upcoming.',
73
+ ' Providers: google (Google Calendar), outlook (Microsoft Outlook)',
74
+ ' /calendar connect google',
75
+ ' /calendar connect outlook',
76
+ ' (add --device to sign in with a device code instead of a browser)',
77
+ '',
78
+ 'This build has no built-in Google/Microsoft sign-in configured yet — there is no project ' +
79
+ 'client id shipped, so a bare connect will stop at the config step and tell you so. To ' +
80
+ 'connect, you need a client id:',
81
+ ' 1. Register a free OAuth app with the provider (Google Cloud Console, or the Azure portal for Microsoft).',
82
+ ' 2. Copy the client id it gives you — a desktop/public-client app needs no secret.',
83
+ ' 3. Open /agent personal-ops -> "Connect Google Calendar (advanced)" or "Connect Microsoft Outlook (advanced)" and paste it in.',
84
+ ' 4. Run /calendar connect google (or outlook) again to authorize.',
85
+ ].join('\n');
86
+ }
87
+
88
+ /** An unrecognized provider word (not a missing one) keeps the short usage form. */
89
+ function unknownProviderMessage(word: string): string {
90
+ return `Unknown calendar provider '${word}'. Usage: /calendar connect <google|outlook> [--device]`;
91
+ }
92
+
93
+ /** Best-effort browser open; never throws, never blocks the flow. */
94
+ function bestEffortOpenBrowser(url: string): void {
95
+ void (async () => {
96
+ try {
97
+ const { spawn } = await import('node:child_process');
98
+ const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
99
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
100
+ const child = spawn(opener, args, { stdio: 'ignore', detached: true });
101
+ child.on('error', () => {});
102
+ child.unref();
103
+ } catch {
104
+ // ignore — the URL was already printed for the user to open manually
105
+ }
106
+ })();
107
+ }
108
+
109
+ export async function runCalendarConnect(
110
+ args: readonly string[],
111
+ ctx: CommandContext,
112
+ factory: CalendarServiceFactory = defaultServiceFactory,
113
+ ): Promise<void> {
114
+ const provider = normalizeProvider(args[1]);
115
+ if (!provider) {
116
+ ctx.print(args[1] ? unknownProviderMessage(args[1]) : connectGuideMessage());
117
+ return;
118
+ }
119
+ const label = PROVIDER_LABEL[provider];
120
+ const device = args.includes('--device');
121
+ const service = factory(ctx);
122
+
123
+ ctx.print(`Connecting ${label}...`);
124
+ const result = device
125
+ ? await service.connectDeviceCode(provider, { onDeviceCode: (s) => ctx.print(deviceCodeMessage(label, s)) })
126
+ : await service.connectAuto(provider, {
127
+ openUrl: (url) => {
128
+ ctx.print(authUrlMessage(label, url));
129
+ bestEffortOpenBrowser(url);
130
+ },
131
+ onDeviceCode: (s) => ctx.print(deviceCodeMessage(label, s)),
132
+ });
133
+
134
+ if (!result.ok) {
135
+ ctx.print([
136
+ `Could not connect ${label} (${result.stage ?? 'unknown'} stage).`,
137
+ ` ${result.error ?? 'unknown error'}`,
138
+ ].join('\n'));
139
+ return;
140
+ }
141
+ ctx.print([
142
+ `Connected ${label}.`,
143
+ ` account ${result.account?.label ?? provider}`,
144
+ ` via ${result.transport === 'device-code' ? 'device code' : 'browser sign-in'}`,
145
+ ` scopes ${(result.account?.scopes ?? []).join(', ') || '(none reported)'}`,
146
+ ].join('\n'));
147
+ }
148
+
149
+ export async function runCalendarDisconnect(
150
+ args: readonly string[],
151
+ ctx: CommandContext,
152
+ factory: CalendarServiceFactory = defaultServiceFactory,
153
+ ): Promise<void> {
154
+ const provider = normalizeProvider(args[1]);
155
+ if (!provider) {
156
+ ctx.print('Usage: /calendar disconnect <google|outlook>');
157
+ return;
158
+ }
159
+ const label = PROVIDER_LABEL[provider];
160
+ const service = factory(ctx);
161
+ const { revokedRemotely } = await service.disconnect(provider);
162
+ ctx.print([
163
+ `Disconnected ${label}.`,
164
+ revokedRemotely
165
+ ? ' The access was revoked at the provider and local tokens were cleared.'
166
+ : ' Local tokens were cleared. This provider offers no token-revocation endpoint, so revoke access in your account settings if desired.',
167
+ ].join('\n'));
168
+ }
169
+
170
+ export async function runCalendarAccounts(
171
+ ctx: CommandContext,
172
+ factory: CalendarServiceFactory = defaultServiceFactory,
173
+ ): Promise<void> {
174
+ const service = factory(ctx);
175
+ const accounts = await service.listAccounts();
176
+ if (accounts.length === 0) {
177
+ ctx.print([
178
+ 'Connected calendar accounts',
179
+ ' None. Connect one with /calendar connect google or /calendar connect outlook',
180
+ ].join('\n'));
181
+ return;
182
+ }
183
+ ctx.print([
184
+ `Connected calendar accounts (${accounts.length})`,
185
+ ...accounts.map(({ account, state }) =>
186
+ ` ${PROVIDER_LABEL[account.provider]} ${account.label} [${CONNECTION_STATE_NOTE[state] ?? state}]`,
187
+ ),
188
+ ].join('\n'));
189
+ }
190
+
191
+ /**
192
+ * Best-effort fetch of provider events for the upcoming window, source-labeled. A
193
+ * provider failure (reconnect needed, rate limit, etc.) is reported as an honest note
194
+ * rather than aborting the whole /calendar upcoming view.
195
+ */
196
+ export async function fetchProviderUpcoming(
197
+ ctx: CommandContext,
198
+ window: EventWindow,
199
+ factory: CalendarServiceFactory = defaultServiceFactory,
200
+ ): Promise<{ readonly events: MergedCalendarEvent[]; readonly notes: string[] }> {
201
+ const service = factory(ctx);
202
+ const accounts = await service.listAccounts();
203
+ if (accounts.length === 0) return { events: [], notes: [] };
204
+ const events: MergedCalendarEvent[] = [];
205
+ const notes: string[] = [];
206
+ for (const { account } of accounts) {
207
+ try {
208
+ const config = await service.resolveConfig(account.provider);
209
+ const providerEvents = await service.listEventsForProvider(config, window);
210
+ events.push(...providerEvents);
211
+ } catch (error) {
212
+ notes.push(` ${PROVIDER_LABEL[account.provider]}: could not load events — ${error instanceof Error ? error.message : String(error)}`);
213
+ }
214
+ }
215
+ return { events, notes };
216
+ }
217
+
218
+ /** Render a source label for a merged event. */
219
+ export function sourceLabel(source: MergedCalendarEvent['source']): string {
220
+ switch (source) {
221
+ case 'google-api': return 'google';
222
+ case 'microsoft-graph': return 'outlook';
223
+ case 'ics-feed': return 'feed';
224
+ default: return 'local';
225
+ }
226
+ }