@pellux/goodvibes-agent 0.1.103 → 0.1.105

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to GoodVibes Agent will be recorded here.
4
4
 
5
+ ## 0.1.105 - 2026-06-01
6
+
7
+ - 7ea88c8 Remove development panel surface
8
+
9
+ ## 0.1.104 - 2026-06-01
10
+
11
+ - 37fe253 Add Agent memory workspace controls
12
+
5
13
  ## 0.1.103 - 2026-06-01
6
14
 
7
15
  - ed9895a Add CLI controls for Agent memory
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-agent",
3
- "version": "0.1.103",
3
+ "version": "0.1.105",
4
4
  "private": false,
5
5
  "description": "GoodVibes personal operator assistant TUI with a proactive Agent product brain, isolated Agent Knowledge, local profiles, routines, skills, personas, and explicit build delegation.",
6
6
  "type": "module",
@@ -0,0 +1,16 @@
1
+ const SECRET_PATTERNS: readonly RegExp[] = [
2
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/i,
3
+ /\bsk-[A-Za-z0-9_-]{16,}\b/,
4
+ /\bgh[pousr]_[A-Za-z0-9_]{16,}\b/i,
5
+ /\b(?:password|passwd|api[_-]?key|token|secret)\s*[:=]\s*\S{6,}/i,
6
+ ];
7
+
8
+ export function containsSecretLikeText(text: string): boolean {
9
+ return SECRET_PATTERNS.some((pattern) => pattern.test(text));
10
+ }
11
+
12
+ export function assertNoSecretLikeMemoryText(fields: readonly string[]): void {
13
+ if (fields.some((field) => containsSecretLikeText(field))) {
14
+ throw new Error('Agent memory cannot store secret-looking values. Store a secret reference or remove the sensitive text.');
15
+ }
16
+ }
@@ -16,6 +16,7 @@ import {
16
16
  type ProvenanceLink,
17
17
  type ProvenanceLinkKind,
18
18
  } from '@pellux/goodvibes-sdk/platform/state';
19
+ import { assertNoSecretLikeMemoryText } from '../agent/memory-safety.ts';
19
20
  import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
20
21
  import type { CliCommandOutput } from './types.ts';
21
22
  import type { CliCommandRuntime } from './management.ts';
@@ -40,13 +41,6 @@ const VALUE_OPTIONS = new Set([
40
41
  'task',
41
42
  'turn',
42
43
  ]);
43
- const SECRET_PATTERNS: readonly RegExp[] = [
44
- /-----BEGIN [A-Z ]*PRIVATE KEY-----/i,
45
- /\bsk-[A-Za-z0-9_-]{16,}\b/,
46
- /\bgh[pousr]_[A-Za-z0-9_]{16,}\b/i,
47
- /\b(?:password|passwd|api[_-]?key|token|secret)\s*[:=]\s*\S{6,}/i,
48
- ];
49
-
50
44
  interface CommandSuccess<TData> {
51
45
  readonly ok: true;
52
46
  readonly kind: string;
@@ -197,16 +191,6 @@ function optionalClass(value: string | undefined): MemoryClass | undefined {
197
191
  return requireClass(value);
198
192
  }
199
193
 
200
- function containsSecretLikeText(text: string): boolean {
201
- return SECRET_PATTERNS.some((pattern) => pattern.test(text));
202
- }
203
-
204
- function assertNoSecretLikeText(fields: readonly string[]): void {
205
- if (fields.some((field) => containsSecretLikeText(field))) {
206
- throw new Error('Agent memory cannot store secret-looking values. Store a secret reference or remove the sensitive text.');
207
- }
208
- }
209
-
210
194
  function timestamp(value: number): string {
211
195
  return new Date(value).toISOString().slice(0, 19).replace('T', ' ');
212
196
  }
@@ -385,7 +369,7 @@ function readBundle(path: string): MemoryBundle {
385
369
  const parsed: unknown = JSON.parse(readFileSync(path, 'utf-8'));
386
370
  if (!isMemoryBundle(parsed)) throw new Error('Invalid Agent memory bundle.');
387
371
  for (const record of parsed.records) {
388
- assertNoSecretLikeText([
372
+ assertNoSecretLikeMemoryText([
389
373
  record.summary,
390
374
  record.detail ?? '',
391
375
  ...record.tags,
@@ -454,7 +438,7 @@ async function handleAdd(runtime: CliCommandRuntime, context: MemoryContext, arg
454
438
  if (!summary) return failure(runtime, 'invalid_memory_command', 'Usage: goodvibes-agent memory add <class> <summary> [--scope <scope>] [--detail <text>] [--tags a,b]', 2);
455
439
  const detail = optionValue(options, 'detail');
456
440
  const tags = csvOption(options, 'tags');
457
- assertNoSecretLikeText([summary, detail ?? '', ...(tags ?? [])]);
441
+ assertNoSecretLikeMemoryText([summary, detail ?? '', ...(tags ?? [])]);
458
442
  const reviewState = optionValue(options, 'review-state');
459
443
  if (reviewState !== undefined && !isReviewState(reviewState)) {
460
444
  return failure(runtime, 'invalid_memory_command', `Invalid review state "${reviewState}". Valid: ${VALID_REVIEW_STATES.join(', ')}`, 2);
@@ -0,0 +1,170 @@
1
+ import { createLocalEditor, createProfileEditor } from './agent-workspace-editors.ts';
2
+ import type {
3
+ AgentWorkspaceActionResult,
4
+ AgentWorkspaceCategory,
5
+ AgentWorkspaceCommandDispatcher,
6
+ AgentWorkspaceFocusPane,
7
+ AgentWorkspaceLocalEditor,
8
+ AgentWorkspaceLocalEditorKind,
9
+ AgentWorkspaceLocalOperation,
10
+ AgentWorkspaceRuntimeSnapshot,
11
+ } from './agent-workspace-types.ts';
12
+
13
+ interface AgentWorkspaceActivationHost {
14
+ readonly categories: readonly AgentWorkspaceCategory[];
15
+ readonly selectedCategory: AgentWorkspaceCategory;
16
+ readonly selectedAction: AgentWorkspaceCategory['actions'][number] | null;
17
+ readonly runtimeSnapshot: AgentWorkspaceRuntimeSnapshot | null;
18
+ localEditor: AgentWorkspaceLocalEditor | null;
19
+ focusPane: AgentWorkspaceFocusPane;
20
+ selectedCategoryIndex: number;
21
+ selectedActionIndex: number;
22
+ status: string;
23
+ lastActionResult: AgentWorkspaceActionResult | null;
24
+ submitEditorFieldOrForm(requestRender?: () => void): void;
25
+ focusActions(): void;
26
+ clampSelection(): void;
27
+ moveLocalLibraryItemSelection(kind: AgentWorkspaceLocalEditorKind, delta: number): void;
28
+ applyLocalLibraryOperation(operation: AgentWorkspaceLocalOperation): void;
29
+ hasCommandDispatch(): boolean;
30
+ dispatchWorkspaceCommand: AgentWorkspaceCommandDispatcher;
31
+ }
32
+
33
+ function parseCommand(command: string): { readonly name: string; readonly args: readonly string[] } {
34
+ const trimmed = command.trim().replace(/^\//, '');
35
+ if (!trimmed) return { name: '', args: [] };
36
+ const parts = trimmed.split(/\s+/);
37
+ return { name: parts[0] ?? '', args: parts.slice(1) };
38
+ }
39
+
40
+ export function activateAgentWorkspaceSelection(
41
+ workspace: AgentWorkspaceActivationHost,
42
+ requestRender?: () => void,
43
+ ): void {
44
+ if (workspace.localEditor) {
45
+ workspace.submitEditorFieldOrForm(requestRender);
46
+ return;
47
+ }
48
+ if (workspace.focusPane === 'categories') {
49
+ workspace.focusActions();
50
+ return;
51
+ }
52
+ const action = workspace.selectedAction;
53
+ if (!action) return;
54
+ if (action.kind === 'editor' && action.editorKind) {
55
+ workspace.localEditor = action.editorKind === 'profile'
56
+ ? createProfileEditor(workspace.runtimeSnapshot?.runtimeStarterTemplates ?? [])
57
+ : createLocalEditor(action.editorKind);
58
+ workspace.status = `Editing ${workspace.localEditor.title}.`;
59
+ workspace.lastActionResult = {
60
+ kind: 'guidance',
61
+ title: workspace.localEditor.title,
62
+ detail: workspace.localEditor.message,
63
+ safety: action.safety,
64
+ };
65
+ return;
66
+ }
67
+ if (action.kind === 'local-selection' && action.localKind) {
68
+ workspace.moveLocalLibraryItemSelection(action.localKind, action.selectionDelta ?? 0);
69
+ return;
70
+ }
71
+ if (action.kind === 'local-operation' && action.localOperation) {
72
+ workspace.applyLocalLibraryOperation(action.localOperation);
73
+ return;
74
+ }
75
+ if (action.kind === 'guidance' || !action.command) {
76
+ handleGuidanceOrWorkspaceAction(workspace, action);
77
+ return;
78
+ }
79
+ if (action.safety === 'blocked') {
80
+ workspace.status = `Blocked here: ${action.label}.`;
81
+ workspace.lastActionResult = {
82
+ kind: 'blocked',
83
+ title: `${action.label} is blocked in Agent`,
84
+ detail: action.detail,
85
+ command: action.command,
86
+ safety: action.safety,
87
+ };
88
+ return;
89
+ }
90
+ const parsed = parseCommand(action.command);
91
+ if (!parsed.name) {
92
+ workspace.status = `No command is configured for ${action.label}.`;
93
+ workspace.lastActionResult = {
94
+ kind: 'error',
95
+ title: 'Command unavailable',
96
+ detail: `No command is configured for ${action.label}.`,
97
+ safety: action.safety,
98
+ };
99
+ return;
100
+ }
101
+ if (/<[^>\s]+(?:\s+[^>]*)?>/.test(action.command)) {
102
+ workspace.status = `Placeholder command not dispatched: ${action.command}.`;
103
+ workspace.lastActionResult = {
104
+ kind: 'guidance',
105
+ title: `${action.label} needs details`,
106
+ detail: 'This action is a command template. Close the workspace and run it with real task text instead of placeholder values.',
107
+ command: action.command,
108
+ safety: action.safety,
109
+ };
110
+ return;
111
+ }
112
+ if (!workspace.hasCommandDispatch()) {
113
+ workspace.status = `Command dispatch is not available for ${action.command}.`;
114
+ workspace.lastActionResult = {
115
+ kind: 'error',
116
+ title: 'Command dispatch unavailable',
117
+ detail: `The command ${action.command} cannot be opened from this runtime.`,
118
+ command: action.command,
119
+ safety: action.safety,
120
+ };
121
+ return;
122
+ }
123
+ workspace.status = `Opening ${action.command}.`;
124
+ workspace.lastActionResult = {
125
+ kind: 'dispatched',
126
+ title: `Opening ${action.label}`,
127
+ detail: 'The workspace handed this safe or read-only command to the shell-owned command router.',
128
+ command: action.command,
129
+ safety: action.safety,
130
+ };
131
+ workspace.dispatchWorkspaceCommand(action.command);
132
+ }
133
+
134
+ function handleGuidanceOrWorkspaceAction(
135
+ workspace: AgentWorkspaceActivationHost,
136
+ action: AgentWorkspaceCategory['actions'][number],
137
+ ): void {
138
+ if (action.kind === 'workspace' && action.targetCategoryId) {
139
+ const targetIndex = workspace.categories.findIndex((category) => category.id === action.targetCategoryId);
140
+ if (targetIndex >= 0) {
141
+ workspace.selectedCategoryIndex = targetIndex;
142
+ workspace.selectedActionIndex = 0;
143
+ workspace.focusActions();
144
+ workspace.status = `Opened ${workspace.selectedCategory.label}.`;
145
+ workspace.lastActionResult = {
146
+ kind: 'refreshed',
147
+ title: `Opened ${workspace.selectedCategory.label}`,
148
+ detail: action.detail,
149
+ safety: action.safety,
150
+ };
151
+ workspace.clampSelection();
152
+ return;
153
+ }
154
+ workspace.status = `Workspace area unavailable: ${action.targetCategoryId}.`;
155
+ workspace.lastActionResult = {
156
+ kind: 'error',
157
+ title: 'Workspace area unavailable',
158
+ detail: `No Agent workspace category exists for ${action.targetCategoryId}.`,
159
+ safety: action.safety,
160
+ };
161
+ return;
162
+ }
163
+ workspace.status = action.detail;
164
+ workspace.lastActionResult = {
165
+ kind: 'guidance',
166
+ title: action.label,
167
+ detail: action.detail,
168
+ safety: action.safety,
169
+ };
170
+ }
@@ -110,7 +110,14 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
110
110
  summary: 'Local assistant memory, routines, skills, and reusable behavior.',
111
111
  detail: 'Memory, routines, skills, and personas stay Agent-local until stable shared registry contracts exist. Secrets must not be stored as memory.',
112
112
  actions: [
113
- { id: 'memory', label: 'Open memory', detail: 'Inspect local/session memory records and commands.', command: '/memory', kind: 'command', safety: 'read-only' },
113
+ { id: 'memory-list', label: 'List memory', detail: 'Print the full Agent-owned memory list.', command: '/memory list', kind: 'command', safety: 'read-only' },
114
+ { id: 'memory-prev', label: 'Previous memory', detail: 'Move the local memory selection up without changing review state.', localKind: 'memory', selectionDelta: -1, kind: 'local-selection', safety: 'safe' },
115
+ { id: 'memory-next', label: 'Next memory', detail: 'Move the local memory selection down without changing review state.', localKind: 'memory', selectionDelta: 1, kind: 'local-selection', safety: 'safe' },
116
+ { id: 'memory-create', label: 'Create memory', detail: 'Open an in-workspace form for a durable, non-secret Agent memory. No default wiki fallback is used.', editorKind: 'memory', kind: 'editor', safety: 'safe' },
117
+ { id: 'memory-edit', label: 'Edit selected memory', detail: 'Open the selected Agent memory in an in-workspace editor.', localKind: 'memory', localOperation: 'memory-edit', kind: 'local-operation', safety: 'safe' },
118
+ { id: 'memory-review', label: 'Review selected', detail: 'Mark the selected Agent memory reviewed after inspecting it.', localKind: 'memory', localOperation: 'memory-review', kind: 'local-operation', safety: 'safe' },
119
+ { id: 'memory-stale', label: 'Mark selected stale', detail: 'Mark the selected Agent memory stale so it stops being trusted until reviewed.', localKind: 'memory', localOperation: 'memory-stale', kind: 'local-operation', safety: 'safe' },
120
+ { id: 'memory-delete', label: 'Delete selected memory', detail: 'Open a confirmation form before deleting the selected Agent memory.', localKind: 'memory', localOperation: 'memory-delete', kind: 'local-operation', safety: 'safe' },
114
121
  { id: 'personas', label: 'Persona library', detail: 'Open the local persona workspace for active role selection and review.', targetCategoryId: 'personas', kind: 'workspace', safety: 'safe' },
115
122
  { id: 'skills', label: 'Local skill library', detail: 'Open the local skill workspace for reusable procedures and review.', targetCategoryId: 'skills', kind: 'workspace', safety: 'safe' },
116
123
  { id: 'routines', label: 'Routine library', detail: 'Open the local routine workspace for repeatable workflows and schedule promotion review.', targetCategoryId: 'routines', kind: 'workspace', safety: 'safe' },
@@ -1,6 +1,7 @@
1
1
  import type { AgentPersonaRecord } from '../agent/persona-registry.ts';
2
2
  import type { AgentRoutineRecord } from '../agent/routine-registry.ts';
3
3
  import type { AgentSkillRecord } from '../agent/skill-registry.ts';
4
+ import type { MemoryRecord } from '@pellux/goodvibes-sdk/platform/state';
4
5
  import type {
5
6
  AgentWorkspaceLocalEditor,
6
7
  AgentWorkspaceLocalEditorKind,
@@ -31,6 +32,23 @@ export function createProfileEditor(templates: readonly AgentWorkspaceRuntimeSta
31
32
 
32
33
  export function createLocalEditor(kind: AgentWorkspaceLocalEditorKind): AgentWorkspaceLocalEditor {
33
34
  if (kind === 'profile') return createProfileEditor([]);
35
+ if (kind === 'memory') {
36
+ return {
37
+ kind,
38
+ mode: 'create',
39
+ title: 'Create Memory',
40
+ selectedFieldIndex: 0,
41
+ message: 'Record a durable, non-secret Agent memory. This stays in the Agent-owned memory store and never writes to default Knowledge/Wiki.',
42
+ fields: [
43
+ { id: 'cls', label: 'Class', value: 'fact', required: true, multiline: false, hint: 'fact, decision, constraint, incident, pattern, risk, runbook, architecture, or ownership.' },
44
+ { id: 'scope', label: 'Scope', value: 'project', required: true, multiline: false, hint: 'session, project, or team.' },
45
+ { id: 'summary', label: 'Summary', value: '', required: true, multiline: false, hint: 'One durable sentence. Do not store secrets.' },
46
+ { id: 'detail', label: 'Detail', value: '', required: false, multiline: true, hint: 'Optional supporting detail. Ctrl-J inserts a new line.' },
47
+ { id: 'tags', label: 'Tags', value: '', required: false, multiline: false, hint: 'Comma-separated optional tags.' },
48
+ { id: 'confidence', label: 'Confidence', value: '80', required: false, multiline: false, hint: '0-100 confidence score.' },
49
+ ],
50
+ };
51
+ }
34
52
  if (kind === 'persona') {
35
53
  return {
36
54
  kind,
@@ -82,6 +100,23 @@ export function createLocalEditor(kind: AgentWorkspaceLocalEditorKind): AgentWor
82
100
  };
83
101
  }
84
102
 
103
+ export function createMemoryUpdateEditor(record: MemoryRecord): AgentWorkspaceLocalEditor {
104
+ return {
105
+ kind: 'memory',
106
+ mode: 'update',
107
+ recordId: record.id,
108
+ title: 'Edit Memory',
109
+ selectedFieldIndex: 0,
110
+ message: `Editing ${record.id}. Saving updates only the Agent-owned memory record.`,
111
+ fields: [
112
+ { id: 'scope', label: 'Scope', value: record.scope, required: true, multiline: false, hint: 'session, project, or team.' },
113
+ { id: 'summary', label: 'Summary', value: record.summary, required: true, multiline: false, hint: 'One durable sentence. Do not store secrets.' },
114
+ { id: 'detail', label: 'Detail', value: record.detail ?? '', required: false, multiline: true, hint: 'Optional supporting detail. Ctrl-J inserts a new line.' },
115
+ { id: 'tags', label: 'Tags', value: record.tags.join(', '), required: false, multiline: false, hint: 'Comma-separated optional tags.' },
116
+ ],
117
+ };
118
+ }
119
+
85
120
  export function createPersonaUpdateEditor(record: AgentPersonaRecord, active: boolean): AgentWorkspaceLocalEditor {
86
121
  return {
87
122
  kind: 'persona',
@@ -164,6 +199,7 @@ export function isAffirmative(value: string): boolean {
164
199
  }
165
200
 
166
201
  export function editorCategoryId(kind: AgentWorkspaceLocalEditorKind): string {
202
+ if (kind === 'memory') return 'memory';
167
203
  if (kind === 'profile') return 'profiles';
168
204
  if (kind === 'persona') return 'personas';
169
205
  if (kind === 'skill') return 'skills';
@@ -0,0 +1,88 @@
1
+ import type { MemoryApi } from '@pellux/goodvibes-sdk/platform/knowledge';
2
+ import type { MemoryClass, MemoryRecord, MemoryScope } from '@pellux/goodvibes-sdk/platform/state';
3
+ import { assertNoSecretLikeMemoryText } from '../agent/memory-safety.ts';
4
+ import type { AgentWorkspaceLocalEditor } from './agent-workspace-types.ts';
5
+ import { splitList } from './agent-workspace-editors.ts';
6
+ import { isValidClass, isValidScope } from './commands/recall-shared.ts';
7
+
8
+ export type AgentWorkspaceEditorFieldReader = (id: string) => string;
9
+
10
+ export interface AgentWorkspaceMemoryEditorResult {
11
+ readonly record: MemoryRecord;
12
+ readonly verb: 'Created' | 'Updated';
13
+ }
14
+
15
+ export interface AgentWorkspaceMemoryDeleteResult {
16
+ readonly id: string;
17
+ readonly name: string;
18
+ }
19
+
20
+ export async function submitAgentWorkspaceMemoryEditor(
21
+ editor: AgentWorkspaceLocalEditor,
22
+ memory: MemoryApi,
23
+ readField: AgentWorkspaceEditorFieldReader,
24
+ ): Promise<AgentWorkspaceMemoryEditorResult> {
25
+ if (editor.mode === 'update' && editor.recordId) {
26
+ const scope = parseMemoryScope(readField('scope'));
27
+ const summary = readField('summary');
28
+ const detail = readField('detail');
29
+ const tags = splitList(readField('tags'));
30
+ assertNoSecretLikeMemoryText([summary, detail, ...tags]);
31
+ const updated = memory.update(editor.recordId, {
32
+ scope,
33
+ summary,
34
+ detail: detail.length > 0 ? detail : undefined,
35
+ tags,
36
+ });
37
+ if (!updated) throw new Error(`Unknown Agent memory: ${editor.recordId}`);
38
+ return { record: updated, verb: 'Updated' };
39
+ }
40
+
41
+ const cls = parseMemoryClass(readField('cls'));
42
+ const scope = parseMemoryScope(readField('scope'));
43
+ const summary = readField('summary');
44
+ const detail = readField('detail');
45
+ const tags = splitList(readField('tags'));
46
+ const confidence = parseMemoryConfidence(readField('confidence'));
47
+ assertNoSecretLikeMemoryText([summary, detail, ...tags]);
48
+ const record = await memory.add({
49
+ cls,
50
+ scope,
51
+ summary,
52
+ detail: detail.length > 0 ? detail : undefined,
53
+ tags,
54
+ review: {
55
+ state: 'fresh',
56
+ confidence,
57
+ },
58
+ });
59
+ return { record, verb: 'Created' };
60
+ }
61
+
62
+ export function deleteAgentWorkspaceMemoryEditor(
63
+ editor: AgentWorkspaceLocalEditor,
64
+ confirmedId: string,
65
+ memory: MemoryApi,
66
+ ): AgentWorkspaceMemoryDeleteResult | null {
67
+ const expectedId = editor.recordId ?? '';
68
+ if (!expectedId || confirmedId !== expectedId) return null;
69
+ const removed = memory.delete(expectedId);
70
+ if (!removed) throw new Error(`Unknown Agent memory: ${expectedId}`);
71
+ return { id: expectedId, name: expectedId };
72
+ }
73
+
74
+ function parseMemoryClass(value: string): MemoryClass {
75
+ if (!isValidClass(value)) throw new Error(`Invalid memory class "${value}".`);
76
+ return value;
77
+ }
78
+
79
+ function parseMemoryScope(value: string): MemoryScope {
80
+ if (!isValidScope(value)) throw new Error(`Invalid memory scope "${value}".`);
81
+ return value;
82
+ }
83
+
84
+ function parseMemoryConfidence(value: string): number {
85
+ const parsed = value.trim().length === 0 ? 80 : Number.parseInt(value, 10);
86
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 100) throw new Error('Memory confidence must be an integer from 0 to 100.');
87
+ return parsed;
88
+ }
@@ -13,6 +13,8 @@ export interface AgentWorkspaceSetupChecklistInput {
13
13
  readonly model: string;
14
14
  readonly runtimeBaseUrl: string;
15
15
  readonly sessionMemoryCount: number;
16
+ readonly localMemoryCount: number;
17
+ readonly localMemoryReviewQueueCount: number;
16
18
  readonly routineCount: number;
17
19
  readonly enabledRoutineCount: number;
18
20
  readonly skillCount: number;
@@ -97,9 +99,9 @@ export function buildAgentWorkspaceSetupChecklist(input: AgentWorkspaceSetupChec
97
99
  {
98
100
  id: 'memory',
99
101
  label: 'Local memory',
100
- status: setupStatusForCount(input.sessionMemoryCount, 'ready', 'optional'),
101
- detail: input.sessionMemoryCount > 0
102
- ? `${input.sessionMemoryCount} session memory record(s) are available.`
102
+ status: setupStatusForCount(input.localMemoryCount, 'ready', 'optional'),
103
+ detail: input.localMemoryCount > 0
104
+ ? `${input.localMemoryCount} Agent memory record(s) are available; ${input.localMemoryReviewQueueCount} need review.`
103
105
  : 'Memory starts empty; durable facts should be stored deliberately and never include secrets.',
104
106
  command: '/memory',
105
107
  },
@@ -1,4 +1,5 @@
1
1
  import { basename, sep } from 'node:path';
2
+ import type { MemoryRecord } from '@pellux/goodvibes-sdk/platform/state';
2
3
  import type { CommandContext } from './command-registry.ts';
3
4
  import { AgentPersonaRegistry, type AgentPersonaRecord } from '../agent/persona-registry.ts';
4
5
  import { AgentRoutineRegistry, type AgentRoutineRecord } from '../agent/routine-registry.ts';
@@ -113,6 +114,22 @@ function summarizeRoutineItem(routine: AgentRoutineRecord): AgentWorkspaceLocalL
113
114
  };
114
115
  }
115
116
 
117
+ function summarizeMemoryItem(record: MemoryRecord): AgentWorkspaceLocalLibraryItem {
118
+ const detail = record.detail?.trim();
119
+ return {
120
+ id: record.id,
121
+ name: record.summary,
122
+ description: detail && detail.length > 0 ? detail : `${record.scope}/${record.cls}`,
123
+ reviewState: record.reviewState,
124
+ source: 'agent-memory',
125
+ tags: record.tags,
126
+ triggers: [],
127
+ scope: record.scope,
128
+ cls: record.cls,
129
+ confidence: record.confidence,
130
+ };
131
+ }
132
+
116
133
  function summarizeRuntimeProfile(profile: ReturnType<typeof listAgentRuntimeProfiles>[number]): AgentWorkspaceRuntimeProfileItem {
117
134
  return {
118
135
  id: profile.id,
@@ -154,6 +171,20 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
154
171
  return 0;
155
172
  }
156
173
  })();
174
+ const memorySnapshot = (() => {
175
+ try {
176
+ const memory = context.clients?.agentKnowledgeApi?.memory;
177
+ if (!memory) return { count: 0, reviewQueueCount: 0, items: [] };
178
+ const records = [...memory.getAll()].sort((left, right) => right.updatedAt - left.updatedAt);
179
+ return {
180
+ count: records.length,
181
+ reviewQueueCount: memory.reviewQueue(100).length,
182
+ items: records.map(summarizeMemoryItem),
183
+ };
184
+ } catch {
185
+ return { count: 0, reviewQueueCount: 0, items: [] };
186
+ }
187
+ })();
157
188
  const personaSnapshot = (() => {
158
189
  try {
159
190
  const shellPaths = context.workspace?.shellPaths;
@@ -264,6 +295,8 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
264
295
  model,
265
296
  runtimeBaseUrl,
266
297
  sessionMemoryCount,
298
+ localMemoryCount: memorySnapshot.count,
299
+ localMemoryReviewQueueCount: memorySnapshot.reviewQueueCount,
267
300
  routineCount: routineSnapshot.count,
268
301
  enabledRoutineCount: routineSnapshot.enabled,
269
302
  skillCount: skillSnapshot.count,
@@ -288,6 +321,9 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
288
321
  runtimeBaseUrl,
289
322
  runtimeOwnership: 'external',
290
323
  sessionMemoryCount,
324
+ localMemoryCount: memorySnapshot.count,
325
+ localMemoryReviewQueueCount: memorySnapshot.reviewQueueCount,
326
+ localMemories: memorySnapshot.items,
291
327
  localRoutineCount: routineSnapshot.count,
292
328
  enabledRoutineCount: routineSnapshot.enabled,
293
329
  localRoutines: routineSnapshot.items,
@@ -0,0 +1,51 @@
1
+ import type { InputToken } from '@pellux/goodvibes-sdk/platform/core';
2
+ import type { AgentWorkspace } from './agent-workspace.ts';
3
+
4
+ export function handleAgentWorkspaceToken(
5
+ workspace: AgentWorkspace,
6
+ token: InputToken,
7
+ handleEscape: () => void,
8
+ requestRender: () => void,
9
+ ): boolean {
10
+ if (!workspace.active) return false;
11
+
12
+ if (workspace.localEditor) {
13
+ if (token.type === 'text') {
14
+ workspace.appendEditorText(token.value);
15
+ } else if (token.type === 'key') {
16
+ if (token.logicalName === 'escape') workspace.cancelLocalEditor();
17
+ else if (token.logicalName === 'enter') workspace.submitEditorFieldOrForm(requestRender);
18
+ else if (token.logicalName === 'tab' || token.logicalName === 'down') workspace.moveEditorField(1);
19
+ else if (token.logicalName === 'up') workspace.moveEditorField(-1);
20
+ else if (token.logicalName === 'backspace' || token.logicalName === 'delete') workspace.editorBackspace();
21
+ else if (token.logicalName === 'j' && token.ctrl === true) workspace.appendEditorNewline();
22
+ }
23
+ requestRender();
24
+ return true;
25
+ }
26
+
27
+ if (token.type === 'key') {
28
+ if (token.logicalName === 'escape') {
29
+ handleEscape();
30
+ return true;
31
+ }
32
+ if (token.logicalName === 'enter' || token.logicalName === 'space') workspace.activateSelected(requestRender);
33
+ else if (token.logicalName === 'left') workspace.focusCategories();
34
+ else if (token.logicalName === 'right') workspace.focusActions();
35
+ else if (token.logicalName === 'up') workspace.moveUp();
36
+ else if (token.logicalName === 'down') workspace.moveDown();
37
+ else if (token.logicalName === 'tab') workspace.toggleFocusPane();
38
+ else if (token.logicalName === 'home') workspace.jumpHome();
39
+ else if (token.logicalName === 'end') workspace.jumpEnd();
40
+ } else if (token.type === 'text') {
41
+ if (token.value === 'h') workspace.focusCategories();
42
+ else if (token.value === 'l') workspace.focusActions();
43
+ else if (token.value === 'j') workspace.moveDown();
44
+ else if (token.value === 'k') workspace.moveUp();
45
+ else if (token.value === 'r' || token.value === 'R') workspace.refreshRuntimeSnapshot();
46
+ else if (token.value === ' ') workspace.activateSelected(requestRender);
47
+ }
48
+
49
+ requestRender();
50
+ return true;
51
+ }
@@ -8,9 +8,13 @@ export type AgentWorkspaceFocusPane = 'categories' | 'actions';
8
8
 
9
9
  export type AgentWorkspaceActionKind = 'command' | 'guidance' | 'workspace' | 'editor' | 'local-selection' | 'local-operation';
10
10
 
11
- export type AgentWorkspaceLocalEditorKind = 'persona' | 'skill' | 'routine' | 'profile';
11
+ export type AgentWorkspaceLocalEditorKind = 'memory' | 'persona' | 'skill' | 'routine' | 'profile';
12
12
 
13
13
  export type AgentWorkspaceLocalOperation =
14
+ | 'memory-edit'
15
+ | 'memory-review'
16
+ | 'memory-stale'
17
+ | 'memory-delete'
14
18
  | 'persona-edit'
15
19
  | 'persona-use'
16
20
  | 'persona-review'
@@ -90,6 +94,9 @@ export interface AgentWorkspaceLocalLibraryItem {
90
94
  readonly source: string;
91
95
  readonly tags: readonly string[];
92
96
  readonly triggers: readonly string[];
97
+ readonly scope?: string;
98
+ readonly cls?: string;
99
+ readonly confidence?: number;
93
100
  readonly active?: boolean;
94
101
  readonly enabled?: boolean;
95
102
  readonly startCount?: number;
@@ -123,6 +130,9 @@ export interface AgentWorkspaceRuntimeSnapshot {
123
130
  readonly runtimeBaseUrl: string;
124
131
  readonly runtimeOwnership: 'external';
125
132
  readonly sessionMemoryCount: number;
133
+ readonly localMemoryCount: number;
134
+ readonly localMemoryReviewQueueCount: number;
135
+ readonly localMemories: readonly AgentWorkspaceLocalLibraryItem[];
126
136
  readonly localRoutineCount: number;
127
137
  readonly enabledRoutineCount: number;
128
138
  readonly localRoutines: readonly AgentWorkspaceLocalLibraryItem[];
@@ -1,12 +1,15 @@
1
- import type { InputToken } from '@pellux/goodvibes-sdk/platform/core';
1
+ import type { MemoryApi } from '@pellux/goodvibes-sdk/platform/knowledge';
2
+ import type { MemoryRecord } from '@pellux/goodvibes-sdk/platform/state';
2
3
  import type { ShellPathService } from '@/runtime/index.ts';
3
4
  import type { CommandContext } from './command-registry.ts';
4
5
  import { AgentPersonaRegistry } from '../agent/persona-registry.ts';
5
6
  import { AgentRoutineRegistry } from '../agent/routine-registry.ts';
6
7
  import { createAgentRuntimeProfile, type AgentRuntimeProfileInfo } from '../agent/runtime-profile.ts';
7
8
  import { AgentSkillRegistry } from '../agent/skill-registry.ts';
9
+ import { activateAgentWorkspaceSelection } from './agent-workspace-activation.ts';
8
10
  import { AGENT_WORKSPACE_CATEGORIES } from './agent-workspace-categories.ts';
9
- import { createDeleteEditor, createLocalEditor, createPersonaUpdateEditor, createProfileEditor, createRoutineUpdateEditor, createSkillUpdateEditor, editorCategoryId, isAffirmative, splitList } from './agent-workspace-editors.ts';
11
+ import { createDeleteEditor, createMemoryUpdateEditor, createPersonaUpdateEditor, createRoutineUpdateEditor, createSkillUpdateEditor, editorCategoryId, isAffirmative, splitList } from './agent-workspace-editors.ts';
12
+ import { deleteAgentWorkspaceMemoryEditor, submitAgentWorkspaceMemoryEditor } from './agent-workspace-memory-editor.ts';
10
13
  import { buildAgentWorkspaceRuntimeSnapshot } from './agent-workspace-snapshot.ts';
11
14
  import type { AgentWorkspaceAction, AgentWorkspaceActionResult, AgentWorkspaceCategory, AgentWorkspaceCommandDispatcher, AgentWorkspaceEditorField, AgentWorkspaceFocusPane, AgentWorkspaceLocalEditor, AgentWorkspaceLocalEditorKind, AgentWorkspaceLocalLibraryItem, AgentWorkspaceLocalOperation, AgentWorkspaceRuntimeSnapshot } from './agent-workspace-types.ts';
12
15
 
@@ -26,12 +29,7 @@ export type {
26
29
  } from './agent-workspace-types.ts';
27
30
  export { AGENT_WORKSPACE_MODAL_NAME } from './agent-workspace-types.ts';
28
31
  export { buildAgentWorkspaceRuntimeSnapshot } from './agent-workspace-snapshot.ts';
29
- function parseCommand(command: string): { readonly name: string; readonly args: readonly string[] } {
30
- const trimmed = command.trim().replace(/^\//, '');
31
- if (!trimmed) return { name: '', args: [] };
32
- const parts = trimmed.split(/\s+/);
33
- return { name: parts[0] ?? '', args: parts.slice(1) };
34
- }
32
+ export { handleAgentWorkspaceToken } from './agent-workspace-token.ts';
35
33
 
36
34
  export class AgentWorkspace {
37
35
  public active = false;
@@ -43,6 +41,7 @@ export class AgentWorkspace {
43
41
  public lastActionResult: AgentWorkspaceActionResult | null = null;
44
42
  public localEditor: AgentWorkspaceLocalEditor | null = null;
45
43
  private readonly selectedLibraryItemIndexes: Record<AgentWorkspaceLocalEditorKind, number> = {
44
+ memory: 0,
46
45
  persona: 0,
47
46
  skill: 0,
48
47
  routine: 0,
@@ -207,147 +206,39 @@ export class AgentWorkspace {
207
206
  this.replaceEditorField(editor.selectedFieldIndex, characters.join(''), editor.message);
208
207
  }
209
208
 
210
- submitEditorFieldOrForm(): void {
209
+ submitEditorFieldOrForm(requestRender?: () => void): void {
211
210
  const editor = this.localEditor;
212
211
  if (!editor) return;
213
212
  if (editor.selectedFieldIndex < editor.fields.length - 1) {
214
213
  this.moveEditorField(1);
215
214
  return;
216
215
  }
217
- this.submitLocalEditor();
216
+ this.submitLocalEditor(requestRender);
218
217
  }
219
218
 
220
- activateSelected(): void {
221
- if (this.localEditor) {
222
- this.submitEditorFieldOrForm();
223
- return;
224
- }
225
- if (this.focusPane === 'categories') {
226
- this.focusActions();
227
- return;
228
- }
229
- const action = this.selectedAction;
230
- if (!action) return;
231
- if (action.kind === 'editor' && action.editorKind) {
232
- this.localEditor = action.editorKind === 'profile'
233
- ? createProfileEditor(this.runtimeSnapshot?.runtimeStarterTemplates ?? [])
234
- : createLocalEditor(action.editorKind);
235
- this.status = `Editing ${this.localEditor.title}.`;
236
- this.lastActionResult = {
237
- kind: 'guidance',
238
- title: this.localEditor.title,
239
- detail: this.localEditor.message,
240
- safety: action.safety,
241
- };
242
- return;
243
- }
244
- if (action.kind === 'local-selection' && action.localKind) {
245
- this.moveLocalLibraryItemSelection(action.localKind, action.selectionDelta ?? 0);
246
- return;
247
- }
248
- if (action.kind === 'local-operation' && action.localOperation) {
249
- this.applyLocalLibraryOperation(action.localOperation);
250
- return;
251
- }
252
- if (action.kind === 'guidance' || !action.command) {
253
- if (action.kind === 'workspace' && action.targetCategoryId) {
254
- const targetIndex = this.categories.findIndex((category) => category.id === action.targetCategoryId);
255
- if (targetIndex >= 0) {
256
- this.selectedCategoryIndex = targetIndex;
257
- this.selectedActionIndex = 0;
258
- this.focusActions();
259
- this.status = `Opened ${this.selectedCategory.label}.`;
260
- this.lastActionResult = {
261
- kind: 'refreshed',
262
- title: `Opened ${this.selectedCategory.label}`,
263
- detail: action.detail,
264
- safety: action.safety,
265
- };
266
- this.clampSelection();
267
- return;
268
- }
269
- this.status = `Workspace area unavailable: ${action.targetCategoryId}.`;
270
- this.lastActionResult = {
271
- kind: 'error',
272
- title: 'Workspace area unavailable',
273
- detail: `No Agent workspace category exists for ${action.targetCategoryId}.`,
274
- safety: action.safety,
275
- };
276
- return;
277
- }
278
- this.status = action.detail;
279
- this.lastActionResult = {
280
- kind: 'guidance',
281
- title: action.label,
282
- detail: action.detail,
283
- safety: action.safety,
284
- };
285
- return;
286
- }
287
- if (action.safety === 'blocked') {
288
- this.status = `Blocked here: ${action.label}.`;
289
- this.lastActionResult = {
290
- kind: 'blocked',
291
- title: `${action.label} is blocked in Agent`,
292
- detail: action.detail,
293
- command: action.command,
294
- safety: action.safety,
295
- };
296
- return;
297
- }
298
- const parsed = parseCommand(action.command);
299
- if (!parsed.name) {
300
- this.status = `No command is configured for ${action.label}.`;
301
- this.lastActionResult = {
302
- kind: 'error',
303
- title: 'Command unavailable',
304
- detail: `No command is configured for ${action.label}.`,
305
- safety: action.safety,
306
- };
307
- return;
308
- }
309
- if (/<[^>\s]+(?:\s+[^>]*)?>/.test(action.command)) {
310
- this.status = `Placeholder command not dispatched: ${action.command}.`;
311
- this.lastActionResult = {
312
- kind: 'guidance',
313
- title: `${action.label} needs details`,
314
- detail: 'This action is a command template. Close the workspace and run it with real task text instead of placeholder values.',
315
- command: action.command,
316
- safety: action.safety,
317
- };
318
- return;
319
- }
320
- if (!this.context?.executeCommand || !this.dispatchCommand) {
321
- this.status = `Command dispatch is not available for ${action.command}.`;
322
- this.lastActionResult = {
323
- kind: 'error',
324
- title: 'Command dispatch unavailable',
325
- detail: `The command ${action.command} cannot be opened from this runtime.`,
326
- command: action.command,
327
- safety: action.safety,
328
- };
329
- return;
330
- }
331
- this.status = `Opening ${action.command}.`;
332
- this.lastActionResult = {
333
- kind: 'dispatched',
334
- title: `Opening ${action.label}`,
335
- detail: 'The workspace handed this safe or read-only command to the shell-owned command router.',
336
- command: action.command,
337
- safety: action.safety,
338
- };
339
- this.dispatchCommand(action.command);
219
+ activateSelected(requestRender?: () => void): void {
220
+ activateAgentWorkspaceSelection(this, requestRender);
340
221
  }
341
222
 
342
- private clampSelection(): void {
223
+ hasCommandDispatch(): boolean {
224
+ return Boolean(this.context?.executeCommand && this.dispatchCommand);
225
+ }
226
+
227
+ dispatchWorkspaceCommand(command: string): void {
228
+ this.dispatchCommand?.(command);
229
+ }
230
+
231
+ clampSelection(): void {
343
232
  this.selectedCategoryIndex = Math.max(0, Math.min(this.selectedCategoryIndex, this.categories.length - 1));
344
233
  this.selectedActionIndex = Math.max(0, Math.min(this.selectedActionIndex, this.actions.length - 1));
234
+ this.clampLocalLibrarySelection('memory');
345
235
  this.clampLocalLibrarySelection('persona');
346
236
  this.clampLocalLibrarySelection('skill');
347
237
  this.clampLocalLibrarySelection('routine');
348
238
  }
349
239
 
350
240
  private localLibraryItems(kind: AgentWorkspaceLocalEditorKind): readonly AgentWorkspaceLocalLibraryItem[] {
241
+ if (kind === 'memory') return this.runtimeSnapshot?.localMemories ?? [];
351
242
  if (kind === 'persona') return this.runtimeSnapshot?.localPersonas ?? [];
352
243
  if (kind === 'skill') return this.runtimeSnapshot?.localSkills ?? [];
353
244
  if (kind === 'profile') return [];
@@ -361,7 +252,7 @@ export class AgentWorkspace {
361
252
  : Math.max(0, Math.min(this.selectedLibraryItemIndexes[kind], length - 1));
362
253
  }
363
254
 
364
- private moveLocalLibraryItemSelection(kind: AgentWorkspaceLocalEditorKind, delta: number): void {
255
+ moveLocalLibraryItemSelection(kind: AgentWorkspaceLocalEditorKind, delta: number): void {
365
256
  const items = this.localLibraryItems(kind);
366
257
  if (items.length === 0) {
367
258
  this.status = `No local ${kind} records to select.`;
@@ -384,7 +275,7 @@ export class AgentWorkspace {
384
275
  };
385
276
  }
386
277
 
387
- private applyLocalLibraryOperation(operation: AgentWorkspaceLocalOperation): void {
278
+ applyLocalLibraryOperation(operation: AgentWorkspaceLocalOperation): void {
388
279
  const shellPaths = this.context?.workspace?.shellPaths;
389
280
  if (!shellPaths) {
390
281
  this.status = 'Local Agent registry files are unavailable.';
@@ -412,7 +303,29 @@ export class AgentWorkspace {
412
303
  };
413
304
  return;
414
305
  }
415
- if (operation === 'persona-edit') {
306
+ if (operation === 'memory-edit') {
307
+ const memory = this.memoryApi();
308
+ const record = memory.get(selected.id);
309
+ if (!record) throw new Error(`Unknown Agent memory: ${selected.id}`);
310
+ this.localEditor = createMemoryUpdateEditor(record);
311
+ this.status = `Editing memory: ${record.id}.`;
312
+ this.lastActionResult = {
313
+ kind: 'guidance',
314
+ title: this.localEditor.title,
315
+ detail: this.localEditor.message,
316
+ safety: 'safe',
317
+ };
318
+ } else if (operation === 'memory-review') {
319
+ const record = this.memoryApi().review(selected.id, { state: 'reviewed', confidence: selected.confidence ?? 100, reviewedBy: 'operator' });
320
+ if (!record) throw new Error(`Unknown Agent memory: ${selected.id}`);
321
+ this.finishLocalOperation('memory', `Reviewed memory ${record.id}`, `${record.summary} is marked reviewed.`);
322
+ } else if (operation === 'memory-stale') {
323
+ const record = this.memoryApi().review(selected.id, { state: 'stale', staleReason: 'Marked stale from Agent workspace', reviewedBy: 'operator' });
324
+ if (!record) throw new Error(`Unknown Agent memory: ${selected.id}`);
325
+ this.finishLocalOperation('memory', `Marked memory stale ${record.id}`, `${record.summary} needs review before reuse.`);
326
+ } else if (operation === 'memory-delete') {
327
+ this.openDeleteEditor('memory', selected);
328
+ } else if (operation === 'persona-edit') {
416
329
  const registry = AgentPersonaRegistry.fromShellPaths(shellPaths);
417
330
  const persona = registry.get(selected.id);
418
331
  if (!persona) throw new Error(`Unknown persona: ${selected.id}`);
@@ -492,11 +405,18 @@ export class AgentWorkspace {
492
405
  }
493
406
 
494
407
  private selectedItemForOperation(operation: AgentWorkspaceLocalOperation): AgentWorkspaceLocalLibraryItem | null {
408
+ if (operation.startsWith('memory-')) return this.selectedLocalLibraryItem('memory');
495
409
  if (operation.startsWith('persona-')) return this.selectedLocalLibraryItem('persona');
496
410
  if (operation.startsWith('skill-')) return this.selectedLocalLibraryItem('skill');
497
411
  return this.selectedLocalLibraryItem('routine');
498
412
  }
499
413
 
414
+ private memoryApi(): MemoryApi {
415
+ const memory = this.context?.clients?.agentKnowledgeApi?.memory;
416
+ if (!memory) throw new Error('Agent Memory API is unavailable; refusing default Knowledge/Wiki or non-Agent fallback.');
417
+ return memory;
418
+ }
419
+
500
420
  private finishLocalOperation(kind: AgentWorkspaceLocalEditorKind, title: string, detail: string): void {
501
421
  this.runtimeSnapshot = this.context ? buildAgentWorkspaceRuntimeSnapshot(this.context) : this.runtimeSnapshot;
502
422
  this.clampLocalLibrarySelection(kind);
@@ -538,7 +458,7 @@ export class AgentWorkspace {
538
458
  return editor.fields.find((field) => field.required && field.value.trim().length === 0) ?? null;
539
459
  }
540
460
 
541
- private submitLocalEditor(): void {
461
+ private submitLocalEditor(requestRender?: () => void): void {
542
462
  const editor = this.localEditor;
543
463
  if (!editor) return;
544
464
  const missing = this.missingEditorField();
@@ -552,6 +472,26 @@ export class AgentWorkspace {
552
472
  this.status = `${missing.label} is required.`;
553
473
  return;
554
474
  }
475
+ if (editor.kind === 'memory') {
476
+ if (editor.mode === 'delete') {
477
+ try {
478
+ this.submitMemoryDeleteEditor(editor);
479
+ } catch (error) {
480
+ const detail = error instanceof Error ? error.message : String(error);
481
+ this.localEditor = { ...editor, message: detail };
482
+ this.status = detail;
483
+ this.lastActionResult = {
484
+ kind: 'error',
485
+ title: `${editor.title} failed`,
486
+ detail,
487
+ };
488
+ }
489
+ requestRender?.();
490
+ return;
491
+ }
492
+ void this.submitMemoryEditor(editor).finally(() => requestRender?.());
493
+ return;
494
+ }
555
495
  const shellPaths = this.context?.workspace?.shellPaths;
556
496
  if (!shellPaths) {
557
497
  this.localEditor = { ...editor, message: 'Cannot save because Agent shell paths are unavailable.' };
@@ -679,7 +619,11 @@ export class AgentWorkspace {
679
619
  this.status = 'Deletion not confirmed.';
680
620
  return;
681
621
  }
682
- if (editor.kind === 'persona') {
622
+ if (editor.kind === 'memory') {
623
+ const removed = this.memoryApi().delete(expectedId);
624
+ if (!removed) throw new Error(`Unknown Agent memory: ${expectedId}`);
625
+ this.finishLocalDelete(editor.kind, expectedId, expectedId);
626
+ } else if (editor.kind === 'persona') {
683
627
  const removed = AgentPersonaRegistry.fromShellPaths(shellPaths).deletePersona(expectedId);
684
628
  this.finishLocalDelete(editor.kind, removed.id, removed.name);
685
629
  } else if (editor.kind === 'skill') {
@@ -691,6 +635,56 @@ export class AgentWorkspace {
691
635
  }
692
636
  }
693
637
 
638
+ private submitMemoryDeleteEditor(editor: AgentWorkspaceLocalEditor): void {
639
+ const expectedId = editor.recordId ?? '';
640
+ const confirmedId = this.editorField('confirm');
641
+ const removed = deleteAgentWorkspaceMemoryEditor(editor, confirmedId, this.memoryApi());
642
+ if (!removed) {
643
+ this.localEditor = {
644
+ ...editor,
645
+ message: `Deletion not confirmed. Type ${expectedId} exactly, then press Enter.`,
646
+ };
647
+ this.status = 'Deletion not confirmed.';
648
+ return;
649
+ }
650
+ this.finishLocalDelete(editor.kind, removed.id, removed.name);
651
+ }
652
+
653
+ private async submitMemoryEditor(editor: AgentWorkspaceLocalEditor): Promise<void> {
654
+ try {
655
+ this.status = 'Saving Agent memory...';
656
+ const result = await submitAgentWorkspaceMemoryEditor(editor, this.memoryApi(), (id) => this.editorField(id));
657
+ this.finishMemoryEditor(result.record, result.verb);
658
+ } catch (error) {
659
+ const detail = error instanceof Error ? error.message : String(error);
660
+ this.localEditor = { ...editor, message: detail };
661
+ this.status = detail;
662
+ this.lastActionResult = {
663
+ kind: 'error',
664
+ title: `${editor.title} failed`,
665
+ detail,
666
+ };
667
+ }
668
+ }
669
+
670
+ private finishMemoryEditor(record: MemoryRecord, verb: 'Created' | 'Updated'): void {
671
+ this.localEditor = null;
672
+ this.runtimeSnapshot = this.context ? buildAgentWorkspaceRuntimeSnapshot(this.context) : this.runtimeSnapshot;
673
+ const categoryIndex = this.categories.findIndex((category) => category.id === 'memory');
674
+ if (categoryIndex >= 0) {
675
+ this.selectedCategoryIndex = categoryIndex;
676
+ this.selectedActionIndex = 0;
677
+ }
678
+ this.status = `${verb} memory: ${record.summary}.`;
679
+ this.lastActionResult = {
680
+ kind: 'refreshed',
681
+ title: `${verb} memory`,
682
+ detail: `${record.summary} (${record.id}) was saved to Agent-owned memory only.`,
683
+ safety: 'safe',
684
+ };
685
+ this.clampSelection();
686
+ }
687
+
694
688
  private finishLocalEditor(kind: AgentWorkspaceLocalEditorKind, id: string, name: string, verb: 'Created' | 'Updated'): void {
695
689
  this.localEditor = null;
696
690
  const categoryId = editorCategoryId(kind);
@@ -749,52 +743,3 @@ export class AgentWorkspace {
749
743
  this.clampSelection();
750
744
  }
751
745
  }
752
-
753
- export function handleAgentWorkspaceToken(
754
- workspace: AgentWorkspace,
755
- token: InputToken,
756
- handleEscape: () => void,
757
- requestRender: () => void,
758
- ): boolean {
759
- if (!workspace.active) return false;
760
-
761
- if (workspace.localEditor) {
762
- if (token.type === 'text') {
763
- workspace.appendEditorText(token.value);
764
- } else if (token.type === 'key') {
765
- if (token.logicalName === 'escape') workspace.cancelLocalEditor();
766
- else if (token.logicalName === 'enter') workspace.submitEditorFieldOrForm();
767
- else if (token.logicalName === 'tab' || token.logicalName === 'down') workspace.moveEditorField(1);
768
- else if (token.logicalName === 'up') workspace.moveEditorField(-1);
769
- else if (token.logicalName === 'backspace' || token.logicalName === 'delete') workspace.editorBackspace();
770
- else if (token.logicalName === 'j' && token.ctrl === true) workspace.appendEditorNewline();
771
- }
772
- requestRender();
773
- return true;
774
- }
775
-
776
- if (token.type === 'key') {
777
- if (token.logicalName === 'escape') {
778
- handleEscape();
779
- return true;
780
- }
781
- if (token.logicalName === 'enter' || token.logicalName === 'space') workspace.activateSelected();
782
- else if (token.logicalName === 'left') workspace.focusCategories();
783
- else if (token.logicalName === 'right') workspace.focusActions();
784
- else if (token.logicalName === 'up') workspace.moveUp();
785
- else if (token.logicalName === 'down') workspace.moveDown();
786
- else if (token.logicalName === 'tab') workspace.toggleFocusPane();
787
- else if (token.logicalName === 'home') workspace.jumpHome();
788
- else if (token.logicalName === 'end') workspace.jumpEnd();
789
- } else if (token.type === 'text') {
790
- if (token.value === 'h') workspace.focusCategories();
791
- else if (token.value === 'l') workspace.focusActions();
792
- else if (token.value === 'j') workspace.moveDown();
793
- else if (token.value === 'k') workspace.moveUp();
794
- else if (token.value === 'r' || token.value === 'R') workspace.refreshRuntimeSnapshot();
795
- else if (token.value === ' ') workspace.activateSelected();
796
- }
797
-
798
- requestRender();
799
- return true;
800
- }
@@ -0,0 +1,21 @@
1
+ import type { PanelManager } from '../panel-manager.ts';
2
+ import { CostTrackerPanel } from '../cost-tracker-panel.ts';
3
+ import type { ResolvedBuiltinPanelDeps } from './shared.ts';
4
+ import { requireUiServices } from './shared.ts';
5
+
6
+ export function registerUsagePanels(manager: PanelManager, deps: ResolvedBuiltinPanelDeps): void {
7
+ if (!deps.getOrchestratorUsage) return;
8
+
9
+ const { getOrchestratorUsage, budgetThreshold } = deps;
10
+ manager.registerType({
11
+ id: 'cost',
12
+ name: 'Cost',
13
+ icon: '$',
14
+ category: 'monitoring',
15
+ description: 'Estimated assistant usage costs for this session and explicit delegated work, with budget alerts',
16
+ factory: () => {
17
+ const ui = requireUiServices(deps);
18
+ return new CostTrackerPanel(ui.events.turns, ui.events.agents, getOrchestratorUsage, { budgetThreshold });
19
+ },
20
+ });
21
+ }
@@ -1,6 +1,6 @@
1
1
  import type { PanelManager } from './panel-manager.ts';
2
2
  import { resolveBuiltinPanelDeps, type BuiltinPanelDeps } from './builtin/shared.ts';
3
- import { registerDevelopmentPanels } from './builtin/development.ts';
3
+ import { registerUsagePanels } from './builtin/usage.ts';
4
4
  import { registerOperationsPanels } from './builtin/operations.ts';
5
5
  import { registerAgentPanels } from './builtin/agent.ts';
6
6
  import { registerSessionPanels } from './builtin/session.ts';
@@ -13,7 +13,7 @@ import { registerKnowledgePanels } from './builtin/knowledge.ts';
13
13
  */
14
14
  export function registerBuiltinPanels(manager: PanelManager, deps: BuiltinPanelDeps): void {
15
15
  const resolved = resolveBuiltinPanelDeps(deps);
16
- registerDevelopmentPanels(manager, resolved);
16
+ registerUsagePanels(manager, resolved);
17
17
  registerOperationsPanels(manager, resolved);
18
18
  registerAgentPanels(manager, resolved);
19
19
  registerSessionPanels(manager, resolved);
@@ -65,9 +65,8 @@ const C = {
65
65
  intro: '#94a3b8',
66
66
  } as const;
67
67
 
68
- const CATEGORY_ORDER: PanelCategory[] = ['development', 'agent', 'monitoring', 'session', 'ai'];
68
+ const CATEGORY_ORDER: PanelCategory[] = ['agent', 'monitoring', 'session', 'ai'];
69
69
  const CATEGORY_LABELS: Record<PanelCategory, string> = {
70
- development: 'Development',
71
70
  agent: 'Agent',
72
71
  monitoring: 'Monitoring',
73
72
  session: 'Session',
@@ -1,11 +1,10 @@
1
1
  import type { PanelRegistration, PanelCategory } from './types.ts';
2
2
 
3
3
  /** Display order for panel categories. */
4
- const CATEGORY_ORDER: PanelCategory[] = ['development', 'agent', 'monitoring', 'session', 'ai'];
4
+ const CATEGORY_ORDER: PanelCategory[] = ['agent', 'monitoring', 'session', 'ai'];
5
5
 
6
6
  /** Human-readable labels for each category. */
7
7
  const CATEGORY_LABELS: Record<PanelCategory, string> = {
8
- development: 'Development',
9
8
  agent: 'Agent',
10
9
  monitoring: 'Monitoring',
11
10
  session: 'Session',
@@ -1,7 +1,7 @@
1
1
  import type { Line } from '../types/grid.ts';
2
2
  import type { ComponentResourceContract, ComponentHealthState } from '../runtime/perf/panel-contracts.ts';
3
3
 
4
- export type PanelCategory = 'development' | 'agent' | 'monitoring' | 'session' | 'ai';
4
+ export type PanelCategory = 'agent' | 'monitoring' | 'session' | 'ai';
5
5
 
6
6
  export interface Panel {
7
7
  id: string;
@@ -123,6 +123,8 @@ function localLibraryLines(
123
123
  selected ? 'selected' : '',
124
124
  item.active ? 'active' : '',
125
125
  item.enabled === true ? 'enabled' : item.enabled === false ? 'disabled' : '',
126
+ item.scope && item.cls ? `${item.scope}/${item.cls}` : '',
127
+ item.confidence !== undefined ? `${item.confidence}%` : '',
126
128
  item.reviewState,
127
129
  item.startCount !== undefined ? `starts ${item.startCount}` : '',
128
130
  ].filter(Boolean).join(' / ');
@@ -301,11 +303,14 @@ function snapshotLines(workspace: AgentWorkspace, category: AgentWorkspaceCatego
301
303
  } else if (category.id === 'memory') {
302
304
  base.push(
303
305
  { text: `Session memories: ${snapshot.sessionMemoryCount}`, fg: PALETTE.info },
306
+ { text: `Agent memory: ${snapshot.localMemoryCount}; review queue: ${snapshot.localMemoryReviewQueueCount}`, fg: PALETTE.info },
304
307
  { text: `Local routines: ${snapshot.localRoutineCount}; enabled: ${snapshot.enabledRoutineCount}`, fg: PALETTE.info },
305
308
  { text: `Local skills: ${snapshot.localSkillCount}; enabled: ${snapshot.enabledSkillCount}; bundles: ${snapshot.localSkillBundleCount}; active skills: ${snapshot.activeSkillCount}`, fg: PALETTE.info },
306
309
  { text: `Local personas: ${snapshot.localPersonaCount}; active: ${snapshot.activePersonaName}`, fg: PALETTE.info },
307
310
  { text: 'Durable memory, routines, skills, and personas remain Agent-local until shared registry contracts exist.', fg: PALETTE.good },
308
311
  { text: 'Secrets are rejected/redacted; store secret references instead of secret values.', fg: PALETTE.warn },
312
+ { text: '' },
313
+ ...localLibraryLines('Agent Memory', snapshot.localMemories, 'No Agent memory yet. Create one here with Create memory.', workspace.selectedLocalLibraryItem('memory')?.id ?? null),
309
314
  );
310
315
  } else if (category.id === 'personas') {
311
316
  base.push(
package/src/version.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '0.1.103';
9
+ let _version = '0.1.105';
10
10
  let _sdkVersion = '0.33.35';
11
11
  try {
12
12
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8')) as {
@@ -1,31 +0,0 @@
1
- import type { PanelManager } from '../panel-manager.ts';
2
- import { PlanDashboardPanel } from '../plan-dashboard-panel.ts';
3
- import { CostTrackerPanel } from '../cost-tracker-panel.ts';
4
- import type { ResolvedBuiltinPanelDeps } from './shared.ts';
5
- import { requireUiServices } from './shared.ts';
6
-
7
- export function registerDevelopmentPanels(manager: PanelManager, deps: ResolvedBuiltinPanelDeps): void {
8
- manager.registerType({
9
- id: 'plan',
10
- name: 'Plan',
11
- icon: 'P',
12
- category: 'agent',
13
- description: 'Active execution plan with phase progress and item status',
14
- factory: () => new PlanDashboardPanel(deps.planManager),
15
- });
16
-
17
- if (deps.getOrchestratorUsage) {
18
- const { getOrchestratorUsage, budgetThreshold } = deps;
19
- manager.registerType({
20
- id: 'cost',
21
- name: 'Cost',
22
- icon: '$',
23
- category: 'monitoring',
24
- description: 'Estimated costs per session, agent, and plan with budget alerts',
25
- factory: () => {
26
- const ui = requireUiServices(deps);
27
- return new CostTrackerPanel(ui.events.turns, ui.events.agents, getOrchestratorUsage, { budgetThreshold });
28
- },
29
- });
30
- }
31
- }