@pellux/goodvibes-agent 0.1.50 → 0.1.51

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,10 @@
2
2
 
3
3
  All notable changes to GoodVibes Agent will be recorded here.
4
4
 
5
+ ## 0.1.51 - 2026-05-31
6
+
7
+ - 6a8e8a6 Add local library workspace editors
8
+
5
9
  ## 0.1.50 - 2026-05-31
6
10
 
7
11
  - bdb654a Improve local library workspaces
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-agent",
3
- "version": "0.1.50",
3
+ "version": "0.1.51",
4
4
  "private": false,
5
5
  "description": "Near-fork GoodVibes operator assistant with the GoodVibes TUI shell, renderer, input, fullscreen workspace, and daemon-connected Agent product brain.",
6
6
  "type": "module",
@@ -20,7 +20,26 @@ export const AGENT_WORKSPACE_MODAL_NAME = 'agentWorkspace';
20
20
 
21
21
  export type AgentWorkspaceFocusPane = 'categories' | 'actions';
22
22
 
23
- export type AgentWorkspaceActionKind = 'command' | 'guidance' | 'workspace';
23
+ export type AgentWorkspaceActionKind = 'command' | 'guidance' | 'workspace' | 'editor';
24
+
25
+ export type AgentWorkspaceLocalEditorKind = 'persona' | 'skill' | 'routine';
26
+
27
+ export interface AgentWorkspaceEditorField {
28
+ readonly id: string;
29
+ readonly label: string;
30
+ readonly value: string;
31
+ readonly required: boolean;
32
+ readonly multiline: boolean;
33
+ readonly hint: string;
34
+ }
35
+
36
+ export interface AgentWorkspaceLocalEditor {
37
+ readonly kind: AgentWorkspaceLocalEditorKind;
38
+ readonly title: string;
39
+ readonly fields: readonly AgentWorkspaceEditorField[];
40
+ readonly selectedFieldIndex: number;
41
+ readonly message: string;
42
+ }
24
43
 
25
44
  export interface AgentWorkspaceAction {
26
45
  readonly id: string;
@@ -28,6 +47,7 @@ export interface AgentWorkspaceAction {
28
47
  readonly detail: string;
29
48
  readonly command?: string;
30
49
  readonly targetCategoryId?: string;
50
+ readonly editorKind?: AgentWorkspaceLocalEditorKind;
31
51
  readonly kind: AgentWorkspaceActionKind;
32
52
  readonly safety: 'safe' | 'read-only' | 'delegates' | 'blocked';
33
53
  }
@@ -495,7 +515,7 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
495
515
  actions: [
496
516
  { id: 'personas-list', label: 'List personas', detail: 'Print the full local persona library.', command: '/personas list', kind: 'command', safety: 'read-only' },
497
517
  { id: 'personas-active', label: 'Show active persona', detail: 'Inspect the active local persona applied to new turns.', command: '/personas active', kind: 'command', safety: 'read-only' },
498
- { id: 'personas-create', label: 'Create persona', detail: 'Create a local persona with real name, summary, and instructions. Placeholder commands are never dispatched.', command: '/personas create --name <name> --description <summary> --body <instructions>', kind: 'command', safety: 'safe' },
518
+ { id: 'personas-create', label: 'Create persona', detail: 'Open an in-workspace form for a local persona. No placeholder command is dispatched.', editorKind: 'persona', kind: 'editor', safety: 'safe' },
499
519
  { id: 'personas-use', label: 'Use persona', detail: 'Activate a local persona by id or name.', command: '/personas use <id>', kind: 'command', safety: 'safe' },
500
520
  { id: 'personas-review', label: 'Review persona', detail: 'Mark a local persona reviewed after inspecting it.', command: '/personas review <id>', kind: 'command', safety: 'safe' },
501
521
  { id: 'personas-clear', label: 'Clear active persona', detail: 'Return to the default Agent policy without deleting any persona.', command: '/personas clear', kind: 'command', safety: 'safe' },
@@ -510,7 +530,7 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
510
530
  actions: [
511
531
  { id: 'skills-list', label: 'List skills', detail: 'Print the full local Agent skill library.', command: '/agent-skills list', kind: 'command', safety: 'read-only' },
512
532
  { id: 'skills-enabled', label: 'Enabled skills', detail: 'Show only skills currently injected into Agent guidance.', command: '/agent-skills enabled', kind: 'command', safety: 'read-only' },
513
- { id: 'skills-create', label: 'Create skill', detail: 'Create a reusable local procedure with real details. Placeholder commands are never dispatched.', command: '/agent-skills create --name <name> --description <summary> --procedure <steps>', kind: 'command', safety: 'safe' },
533
+ { id: 'skills-create', label: 'Create skill', detail: 'Open an in-workspace form for a reusable local procedure. No placeholder command is dispatched.', editorKind: 'skill', kind: 'editor', safety: 'safe' },
514
534
  { id: 'skills-enable', label: 'Enable skill', detail: 'Enable a local Agent skill by id or name.', command: '/agent-skills enable <id>', kind: 'command', safety: 'safe' },
515
535
  { id: 'skills-review', label: 'Review skill', detail: 'Mark a local skill reviewed after inspecting it.', command: '/agent-skills review <id>', kind: 'command', safety: 'safe' },
516
536
  ],
@@ -524,7 +544,7 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
524
544
  actions: [
525
545
  { id: 'routines-list', label: 'List routines', detail: 'Print the full local Agent routine library.', command: '/routines list', kind: 'command', safety: 'read-only' },
526
546
  { id: 'routines-enabled', label: 'Enabled routines', detail: 'Show routines available for direct use.', command: '/routines enabled', kind: 'command', safety: 'read-only' },
527
- { id: 'routines-create', label: 'Create routine', detail: 'Create a repeatable workflow with real steps. Placeholder commands are never dispatched.', command: '/routines create --name <name> --description <summary> --steps <steps>', kind: 'command', safety: 'safe' },
547
+ { id: 'routines-create', label: 'Create routine', detail: 'Open an in-workspace form for a repeatable local workflow. No placeholder command is dispatched.', editorKind: 'routine', kind: 'editor', safety: 'safe' },
528
548
  { id: 'routines-start', label: 'Start routine', detail: 'Start a local routine in the main conversation without creating a hidden job.', command: '/routines start <id>', kind: 'command', safety: 'safe' },
529
549
  { id: 'routines-promote', label: 'Promote to schedule', detail: 'Create an external daemon schedule from a reviewed routine only with real timing and --yes.', command: '/routines promote <id> --cron <expr> --yes', kind: 'command', safety: 'safe' },
530
550
  { id: 'routines-receipts', label: 'Promotion receipts', detail: 'Inspect local redacted routine schedule promotion receipts.', command: '/routines receipts', kind: 'command', safety: 'read-only' },
@@ -578,6 +598,70 @@ function parseCommand(command: string): { readonly name: string; readonly args:
578
598
  return { name: parts[0] ?? '', args: parts.slice(1) };
579
599
  }
580
600
 
601
+ function createLocalEditor(kind: AgentWorkspaceLocalEditorKind): AgentWorkspaceLocalEditor {
602
+ if (kind === 'persona') {
603
+ return {
604
+ kind,
605
+ title: 'Create Persona',
606
+ selectedFieldIndex: 0,
607
+ message: 'Enter a local behavior profile for the serial main-conversation assistant.',
608
+ fields: [
609
+ { id: 'name', label: 'Name', value: '', required: true, multiline: false, hint: 'Short persona name.' },
610
+ { id: 'description', label: 'Description', value: '', required: true, multiline: false, hint: 'One-line summary of when to use it.' },
611
+ { id: 'body', label: 'Instructions', value: '', required: true, multiline: true, hint: 'Operating guidance. Ctrl-J inserts a new line.' },
612
+ { id: 'tags', label: 'Tags', value: '', required: false, multiline: false, hint: 'Comma-separated optional tags.' },
613
+ { id: 'triggers', label: 'Triggers', value: '', required: false, multiline: false, hint: 'Comma-separated words that suggest this persona.' },
614
+ { id: 'activate', label: 'Activate now', value: 'yes', required: false, multiline: false, hint: 'yes/no.' },
615
+ ],
616
+ };
617
+ }
618
+ if (kind === 'skill') {
619
+ return {
620
+ kind,
621
+ title: 'Create Skill',
622
+ selectedFieldIndex: 0,
623
+ message: 'Enter a reusable local procedure the assistant can apply from the main conversation.',
624
+ fields: [
625
+ { id: 'name', label: 'Name', value: '', required: true, multiline: false, hint: 'Short skill name.' },
626
+ { id: 'description', label: 'Description', value: '', required: true, multiline: false, hint: 'One-line summary of the procedure.' },
627
+ { id: 'procedure', label: 'Procedure', value: '', required: true, multiline: true, hint: 'Reusable steps. Ctrl-J inserts a new line.' },
628
+ { id: 'triggers', label: 'Triggers', value: '', required: false, multiline: false, hint: 'Comma-separated words that suggest this skill.' },
629
+ { id: 'tags', label: 'Tags', value: '', required: false, multiline: false, hint: 'Comma-separated optional tags.' },
630
+ { id: 'enabled', label: 'Enable now', value: 'yes', required: false, multiline: false, hint: 'yes/no.' },
631
+ ],
632
+ };
633
+ }
634
+ return {
635
+ kind,
636
+ title: 'Create Routine',
637
+ selectedFieldIndex: 0,
638
+ message: 'Enter a repeatable workflow. It runs in the main conversation unless explicitly promoted to a daemon schedule.',
639
+ fields: [
640
+ { id: 'name', label: 'Name', value: '', required: true, multiline: false, hint: 'Short routine name.' },
641
+ { id: 'description', label: 'Description', value: '', required: true, multiline: false, hint: 'One-line summary of the workflow.' },
642
+ { id: 'steps', label: 'Steps', value: '', required: true, multiline: true, hint: 'Workflow steps. Ctrl-J inserts a new line.' },
643
+ { id: 'triggers', label: 'Triggers', value: '', required: false, multiline: false, hint: 'Comma-separated words that suggest this routine.' },
644
+ { id: 'tags', label: 'Tags', value: '', required: false, multiline: false, hint: 'Comma-separated optional tags.' },
645
+ { id: 'enabled', label: 'Enable now', value: 'yes', required: false, multiline: false, hint: 'yes/no.' },
646
+ ],
647
+ };
648
+ }
649
+
650
+ function splitList(value: string): string[] {
651
+ return value.split(',').map((part) => part.trim()).filter(Boolean);
652
+ }
653
+
654
+ function isAffirmative(value: string): boolean {
655
+ const normalized = value.trim().toLowerCase();
656
+ return normalized === '' || normalized === 'yes' || normalized === 'y' || normalized === 'true' || normalized === 'enabled' || normalized === 'on';
657
+ }
658
+
659
+ function editorCategoryId(kind: AgentWorkspaceLocalEditorKind): string {
660
+ if (kind === 'persona') return 'personas';
661
+ if (kind === 'skill') return 'skills';
662
+ return 'routines';
663
+ }
664
+
581
665
  export class AgentWorkspace {
582
666
  public active = false;
583
667
  public focusPane: AgentWorkspaceFocusPane = 'actions';
@@ -586,6 +670,7 @@ export class AgentWorkspace {
586
670
  public status = 'Ready. Choose an operator flow; ordinary assistant work stays in the main conversation.';
587
671
  public runtimeSnapshot: AgentWorkspaceRuntimeSnapshot | null = null;
588
672
  public lastActionResult: AgentWorkspaceActionResult | null = null;
673
+ public localEditor: AgentWorkspaceLocalEditor | null = null;
589
674
  private context: CommandContext | null = null;
590
675
  private dispatchCommand: AgentWorkspaceCommandDispatcher | null = null;
591
676
 
@@ -597,6 +682,7 @@ export class AgentWorkspace {
597
682
  this.focusPane = 'actions';
598
683
  this.status = 'Ready. Choose an operator flow; ordinary assistant work stays in the main conversation.';
599
684
  this.lastActionResult = null;
685
+ this.localEditor = null;
600
686
  this.clampSelection();
601
687
  }
602
688
 
@@ -607,6 +693,7 @@ export class AgentWorkspace {
607
693
 
608
694
  close(): void {
609
695
  this.active = false;
696
+ this.localEditor = null;
610
697
  }
611
698
 
612
699
  get categories(): readonly AgentWorkspaceCategory[] {
@@ -688,13 +775,86 @@ export class AgentWorkspace {
688
775
  };
689
776
  }
690
777
 
778
+ cancelLocalEditor(): void {
779
+ if (!this.localEditor) return;
780
+ const title = this.localEditor.title;
781
+ this.localEditor = null;
782
+ this.status = `${title} cancelled.`;
783
+ this.lastActionResult = {
784
+ kind: 'guidance',
785
+ title: `${title} cancelled`,
786
+ detail: 'No local Agent registry changes were written.',
787
+ };
788
+ }
789
+
790
+ moveEditorField(delta: number): void {
791
+ const editor = this.localEditor;
792
+ if (!editor) return;
793
+ const nextIndex = Math.max(0, Math.min(editor.fields.length - 1, editor.selectedFieldIndex + delta));
794
+ this.localEditor = { ...editor, selectedFieldIndex: nextIndex };
795
+ }
796
+
797
+ appendEditorText(text: string): void {
798
+ const editor = this.localEditor;
799
+ if (!editor || text.length === 0) return;
800
+ const field = editor.fields[editor.selectedFieldIndex];
801
+ if (!field) return;
802
+ this.replaceEditorField(editor.selectedFieldIndex, `${field.value}${text}`, editor.message);
803
+ }
804
+
805
+ appendEditorNewline(): void {
806
+ const editor = this.localEditor;
807
+ if (!editor) return;
808
+ const field = editor.fields[editor.selectedFieldIndex];
809
+ if (!field || !field.multiline) {
810
+ this.moveEditorField(1);
811
+ return;
812
+ }
813
+ this.replaceEditorField(editor.selectedFieldIndex, `${field.value}\n`, editor.message);
814
+ }
815
+
816
+ editorBackspace(): void {
817
+ const editor = this.localEditor;
818
+ if (!editor) return;
819
+ const field = editor.fields[editor.selectedFieldIndex];
820
+ if (!field || field.value.length === 0) return;
821
+ const characters = Array.from(field.value);
822
+ characters.pop();
823
+ this.replaceEditorField(editor.selectedFieldIndex, characters.join(''), editor.message);
824
+ }
825
+
826
+ submitEditorFieldOrForm(): void {
827
+ const editor = this.localEditor;
828
+ if (!editor) return;
829
+ if (editor.selectedFieldIndex < editor.fields.length - 1) {
830
+ this.moveEditorField(1);
831
+ return;
832
+ }
833
+ this.submitLocalEditor();
834
+ }
835
+
691
836
  activateSelected(): void {
837
+ if (this.localEditor) {
838
+ this.submitEditorFieldOrForm();
839
+ return;
840
+ }
692
841
  if (this.focusPane === 'categories') {
693
842
  this.focusActions();
694
843
  return;
695
844
  }
696
845
  const action = this.selectedAction;
697
846
  if (!action) return;
847
+ if (action.kind === 'editor' && action.editorKind) {
848
+ this.localEditor = createLocalEditor(action.editorKind);
849
+ this.status = `Editing ${this.localEditor.title}.`;
850
+ this.lastActionResult = {
851
+ kind: 'guidance',
852
+ title: this.localEditor.title,
853
+ detail: this.localEditor.message,
854
+ safety: action.safety,
855
+ };
856
+ return;
857
+ }
698
858
  if (action.kind === 'guidance' || !action.command) {
699
859
  if (action.kind === 'workspace' && action.targetCategoryId) {
700
860
  const targetIndex = this.categories.findIndex((category) => category.id === action.targetCategoryId);
@@ -789,6 +949,121 @@ export class AgentWorkspace {
789
949
  this.selectedCategoryIndex = Math.max(0, Math.min(this.selectedCategoryIndex, this.categories.length - 1));
790
950
  this.selectedActionIndex = Math.max(0, Math.min(this.selectedActionIndex, this.actions.length - 1));
791
951
  }
952
+
953
+ private replaceEditorField(index: number, value: string, message: string): void {
954
+ const editor = this.localEditor;
955
+ if (!editor) return;
956
+ const fields = editor.fields.map((field, fieldIndex) => fieldIndex === index ? { ...field, value } : field);
957
+ this.localEditor = { ...editor, fields, message };
958
+ }
959
+
960
+ private editorField(id: string): string {
961
+ const editor = this.localEditor;
962
+ return editor?.fields.find((field) => field.id === id)?.value.trim() ?? '';
963
+ }
964
+
965
+ private missingEditorField(): AgentWorkspaceEditorField | null {
966
+ const editor = this.localEditor;
967
+ if (!editor) return null;
968
+ return editor.fields.find((field) => field.required && field.value.trim().length === 0) ?? null;
969
+ }
970
+
971
+ private submitLocalEditor(): void {
972
+ const editor = this.localEditor;
973
+ if (!editor) return;
974
+ const missing = this.missingEditorField();
975
+ if (missing) {
976
+ const missingIndex = editor.fields.findIndex((field) => field.id === missing.id);
977
+ this.localEditor = {
978
+ ...editor,
979
+ selectedFieldIndex: Math.max(0, missingIndex),
980
+ message: `${missing.label} is required before saving.`,
981
+ };
982
+ this.status = `${missing.label} is required.`;
983
+ return;
984
+ }
985
+ const shellPaths = this.context?.workspace?.shellPaths;
986
+ if (!shellPaths) {
987
+ this.localEditor = { ...editor, message: 'Cannot save because Agent shell paths are unavailable.' };
988
+ this.status = 'Cannot save local Agent registry item without shell paths.';
989
+ this.lastActionResult = {
990
+ kind: 'error',
991
+ title: 'Local registry unavailable',
992
+ detail: 'The Agent workspace cannot locate the Agent-local registry files for this runtime.',
993
+ };
994
+ return;
995
+ }
996
+ try {
997
+ if (editor.kind === 'persona') {
998
+ const registry = AgentPersonaRegistry.fromShellPaths(shellPaths);
999
+ const created = registry.create({
1000
+ name: this.editorField('name'),
1001
+ description: this.editorField('description'),
1002
+ body: this.editorField('body'),
1003
+ tags: splitList(this.editorField('tags')),
1004
+ triggers: splitList(this.editorField('triggers')),
1005
+ source: 'user',
1006
+ provenance: 'agent-workspace',
1007
+ });
1008
+ if (isAffirmative(this.editorField('activate'))) registry.setActive(created.id);
1009
+ this.finishLocalEditor(editor.kind, created.id, created.name);
1010
+ } else if (editor.kind === 'skill') {
1011
+ const registry = AgentSkillRegistry.fromShellPaths(shellPaths);
1012
+ const created = registry.create({
1013
+ name: this.editorField('name'),
1014
+ description: this.editorField('description'),
1015
+ procedure: this.editorField('procedure'),
1016
+ triggers: splitList(this.editorField('triggers')),
1017
+ tags: splitList(this.editorField('tags')),
1018
+ enabled: isAffirmative(this.editorField('enabled')),
1019
+ source: 'user',
1020
+ provenance: 'agent-workspace',
1021
+ });
1022
+ this.finishLocalEditor(editor.kind, created.id, created.name);
1023
+ } else {
1024
+ const registry = AgentRoutineRegistry.fromShellPaths(shellPaths);
1025
+ const created = registry.create({
1026
+ name: this.editorField('name'),
1027
+ description: this.editorField('description'),
1028
+ steps: this.editorField('steps'),
1029
+ triggers: splitList(this.editorField('triggers')),
1030
+ tags: splitList(this.editorField('tags')),
1031
+ enabled: isAffirmative(this.editorField('enabled')),
1032
+ source: 'user',
1033
+ provenance: 'agent-workspace',
1034
+ });
1035
+ this.finishLocalEditor(editor.kind, created.id, created.name);
1036
+ }
1037
+ } catch (error) {
1038
+ const detail = error instanceof Error ? error.message : String(error);
1039
+ this.localEditor = { ...editor, message: detail };
1040
+ this.status = detail;
1041
+ this.lastActionResult = {
1042
+ kind: 'error',
1043
+ title: `${editor.title} failed`,
1044
+ detail,
1045
+ };
1046
+ }
1047
+ }
1048
+
1049
+ private finishLocalEditor(kind: AgentWorkspaceLocalEditorKind, id: string, name: string): void {
1050
+ this.localEditor = null;
1051
+ const categoryId = editorCategoryId(kind);
1052
+ const categoryIndex = this.categories.findIndex((category) => category.id === categoryId);
1053
+ if (categoryIndex >= 0) {
1054
+ this.selectedCategoryIndex = categoryIndex;
1055
+ this.selectedActionIndex = 0;
1056
+ }
1057
+ this.runtimeSnapshot = this.context ? buildAgentWorkspaceRuntimeSnapshot(this.context) : this.runtimeSnapshot;
1058
+ this.status = `Created ${kind}: ${name}.`;
1059
+ this.lastActionResult = {
1060
+ kind: 'refreshed',
1061
+ title: `Created ${kind}`,
1062
+ detail: `${name} (${id}) was saved to the Agent-local ${categoryId} registry.`,
1063
+ safety: 'safe',
1064
+ };
1065
+ this.clampSelection();
1066
+ }
792
1067
  }
793
1068
 
794
1069
  export function handleAgentWorkspaceToken(
@@ -799,6 +1074,21 @@ export function handleAgentWorkspaceToken(
799
1074
  ): boolean {
800
1075
  if (!workspace.active) return false;
801
1076
 
1077
+ if (workspace.localEditor) {
1078
+ if (token.type === 'text') {
1079
+ workspace.appendEditorText(token.value);
1080
+ } else if (token.type === 'key') {
1081
+ if (token.logicalName === 'escape') workspace.cancelLocalEditor();
1082
+ else if (token.logicalName === 'enter') workspace.submitEditorFieldOrForm();
1083
+ else if (token.logicalName === 'tab' || token.logicalName === 'down') workspace.moveEditorField(1);
1084
+ else if (token.logicalName === 'up') workspace.moveEditorField(-1);
1085
+ else if (token.logicalName === 'backspace' || token.logicalName === 'delete') workspace.editorBackspace();
1086
+ else if (token.logicalName === 'j' && token.ctrl === true) workspace.appendEditorNewline();
1087
+ }
1088
+ requestRender();
1089
+ return true;
1090
+ }
1091
+
802
1092
  if (token.type === 'key') {
803
1093
  if (token.logicalName === 'escape') {
804
1094
  handleEscape();
@@ -3,6 +3,7 @@ import type {
3
3
  AgentWorkspaceAction,
4
4
  AgentWorkspaceActionResult,
5
5
  AgentWorkspaceCategory,
6
+ AgentWorkspaceLocalEditor,
6
7
  AgentWorkspaceRuntimeSnapshot,
7
8
  } from '../input/agent-workspace.ts';
8
9
  import type { Line } from '../types/grid.ts';
@@ -68,6 +69,7 @@ function buildLeftRows(workspace: AgentWorkspace, height: number): WorkspaceRow[
68
69
 
69
70
  function actionCommand(action: AgentWorkspaceAction): string {
70
71
  if (action.kind === 'workspace') return action.targetCategoryId ? `open ${action.targetCategoryId}` : '(workspace)';
72
+ if (action.kind === 'editor') return action.editorKind ? `edit ${action.editorKind}` : '(editor)';
71
73
  return action.command ?? '(guidance)';
72
74
  }
73
75
 
@@ -260,6 +262,23 @@ function snapshotLines(category: AgentWorkspaceCategory, snapshot: AgentWorkspac
260
262
  return base;
261
263
  }
262
264
 
265
+ function editorContextLines(editor: AgentWorkspaceLocalEditor): ContextLine[] {
266
+ const selected = editor.fields[editor.selectedFieldIndex];
267
+ const lines: ContextLine[] = [
268
+ { text: editor.title, fg: PALETTE.title, bold: true },
269
+ { text: editor.message, fg: editor.message.includes('required') || editor.message.includes('cannot') || editor.message.includes('Cannot') ? PALETTE.warn : PALETTE.info },
270
+ { text: 'Enter advances fields and saves from the final field. Ctrl-J adds a line inside multiline fields. Esc cancels without writing.', fg: PALETTE.muted },
271
+ ];
272
+ if (selected) {
273
+ lines.push(
274
+ { text: '' },
275
+ { text: `Editing: ${selected.label}${selected.required ? ' (required)' : ''}`, fg: PALETTE.title, bold: true },
276
+ { text: selected.hint, fg: PALETTE.muted },
277
+ );
278
+ }
279
+ return lines;
280
+ }
281
+
263
282
  function buildContextRows(workspace: AgentWorkspace, category: AgentWorkspaceCategory, action: AgentWorkspaceAction | null, width: number): WorkspaceRow[] {
264
283
  const lines: ContextLine[] = [
265
284
  { text: category.label, fg: PALETTE.title, bold: true },
@@ -267,6 +286,8 @@ function buildContextRows(workspace: AgentWorkspace, category: AgentWorkspaceCat
267
286
  { text: '' },
268
287
  { text: category.detail, fg: PALETTE.text },
269
288
  { text: '' },
289
+ ...(workspace.localEditor ? editorContextLines(workspace.localEditor) : []),
290
+ ...(workspace.localEditor ? [{ text: '' }] : []),
270
291
  ...snapshotLines(category, workspace.runtimeSnapshot),
271
292
  ];
272
293
 
@@ -303,7 +324,42 @@ function buildContextRows(workspace: AgentWorkspace, category: AgentWorkspaceCat
303
324
  });
304
325
  }
305
326
 
327
+ function buildEditorRows(editor: AgentWorkspaceLocalEditor, width: number, height: number): WorkspaceRow[] {
328
+ const rows: WorkspaceRow[] = [
329
+ { text: editor.title, fg: PALETTE.title, bold: true },
330
+ { text: editor.message, fg: PALETTE.info },
331
+ { text: '' },
332
+ ];
333
+ for (let index = 0; index < editor.fields.length; index += 1) {
334
+ const field = editor.fields[index]!;
335
+ const selected = index === editor.selectedFieldIndex;
336
+ const marker = selected ? GLYPHS.navigation.selected : ' ';
337
+ const required = field.required ? ' *' : '';
338
+ const value = field.value.length > 0 ? field.value : '(empty)';
339
+ const color = selected ? PALETTE.text : field.value.length > 0 ? PALETTE.info : PALETTE.muted;
340
+ rows.push({
341
+ text: `${marker} ${field.label}${required}`,
342
+ selected,
343
+ fg: color,
344
+ bold: selected,
345
+ });
346
+ const valueLines = value.split('\n');
347
+ for (const valueLine of valueLines.slice(0, 4)) {
348
+ for (const wrapped of wrapText(` ${valueLine}`, Math.max(1, width - 2))) {
349
+ rows.push({ text: wrapped, fg: field.value.length > 0 ? PALETTE.text : PALETTE.dim, dim: field.value.length === 0 });
350
+ }
351
+ }
352
+ if (valueLines.length > 4) rows.push({ text: ` ${valueLines.length - 4} more line(s)`, fg: PALETTE.dim, dim: true });
353
+ rows.push({ text: ` ${field.hint}`, fg: PALETTE.dim, dim: true });
354
+ }
355
+ rows.push({ text: '' });
356
+ rows.push({ text: 'Enter next/save · Up/Down field · Backspace edit · Ctrl-J newline · Esc cancel', fg: PALETTE.muted });
357
+ while (rows.length < height) rows.push({ text: '', kind: 'empty' });
358
+ return rows.slice(0, height);
359
+ }
360
+
306
361
  function buildActionRows(workspace: AgentWorkspace, width: number, height: number): WorkspaceRow[] {
362
+ if (workspace.localEditor) return buildEditorRows(workspace.localEditor, width, height);
307
363
  const rows: WorkspaceRow[] = [];
308
364
  const labelWidth = Math.min(28, Math.max(16, Math.floor(width * 0.30)));
309
365
  const safetyWidth = 10;
@@ -350,6 +406,9 @@ function buildActionRows(workspace: AgentWorkspace, width: number, height: numbe
350
406
  }
351
407
 
352
408
  function footerText(workspace: AgentWorkspace): string {
409
+ if (workspace.localEditor) {
410
+ return `Agent workspace · editing ${workspace.localEditor.kind} · Enter next/save · Ctrl-J newline · Esc cancel`;
411
+ }
353
412
  const focus = workspace.focusPane === 'categories' ? 'categories' : 'actions';
354
413
  return `Agent workspace · focus ${focus} · Up/Down navigate · Left/Right pane · Enter open/action · R refresh · Esc close`;
355
414
  }
@@ -371,7 +430,7 @@ export function renderAgentWorkspace(workspace: AgentWorkspace, width: number, h
371
430
  width,
372
431
  height,
373
432
  title: 'GoodVibes Agent / Operator Workspace',
374
- stateLabel: workspace.focusPane === 'categories' ? 'Categories' : 'Actions',
433
+ stateLabel: workspace.localEditor ? 'Editor' : workspace.focusPane === 'categories' ? 'Categories' : 'Actions',
375
434
  leftHeader: 'Operator Areas',
376
435
  mainHeader: `${category.label} · ${category.actions.length} action(s)`,
377
436
  leftRows: buildLeftRows(workspace, metrics.bodyRows),
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.50';
9
+ let _version = '0.1.51';
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 {