newmark-agent 0.4.0 → 0.4.2

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.
@@ -3,12 +3,15 @@
3
3
  const data = require("./data");
4
4
  const {
5
5
  activeConversationModelLabel,
6
+ buildEventKey,
6
7
  filteredCommands,
8
+ focusableEventsForRun,
7
9
  memoryTagOptions,
8
10
  resolveThemeAppearance,
9
11
  selectedMemoryDetail
10
12
  } = require("./state");
11
13
  const { SETTINGS_CATEGORIES, displaySettingValue, settingsRows } = require("./settings-schema");
14
+ const { tr, resolveLanguage } = require("./i18n");
12
15
 
13
16
  const ESC = "\u001b[";
14
17
  const ANSI_PATTERN = /\u001b\[[0-9;?]*[A-Za-z]/g;
@@ -143,7 +146,7 @@ function card(lines, width, p, title) {
143
146
  }
144
147
 
145
148
  function renderSidebar(state, height, width, p) {
146
- const header = [`${p.brand}${p.bold} NEWMARK${p.reset}`, `${p.muted} Agent TUI${p.reset}`, ""];
149
+ const header = [`${p.brand}${p.bold} NEWMARK${p.reset}`, `${p.muted} ${tr(state, "Agent TUI")}${p.reset}`, ""];
147
150
  const menuLines = [];
148
151
  let focusedLine = 0;
149
152
  const isFocused = (level, index) => state.focusRegion === "menu"
@@ -155,7 +158,7 @@ function renderSidebar(state, height, width, p) {
155
158
  const style = isFocused(level, index) ? `${p.selected}${p.bold}` : active ? p.bold : "";
156
159
  const prefix = " ".repeat(indent);
157
160
  if (isFocused(level, index)) focusedLine = menuLines.length;
158
- menuLines.push(`${marker}${style} ${prefix}${item.icon} ${pad(item.label, width - 7 - indent)}${p.reset}`);
161
+ menuLines.push(`${marker}${style} ${prefix}${item.icon} ${pad(tr(state, item.label), width - 7 - indent)}${p.reset}`);
159
162
  };
160
163
  const addGroup = (label, level, rootIndex, expanded, active, icon = "") => {
161
164
  const focused = isFocused("root", rootIndex);
@@ -165,7 +168,7 @@ function renderSidebar(state, height, width, p) {
165
168
  if (focused) focusedLine = menuLines.length;
166
169
  menuLines.push(`${marker}${style} ${chevron} ${icon ? `${icon} ` : ""}${pad(label, width - 6 - (icon ? 2 : 0))}${p.reset}`);
167
170
  };
168
- menuLines.push(`${p.muted} WORKSPACES${p.reset}`);
171
+ menuLines.push(`${p.muted} ${tr(state, "WORKSPACES")}${p.reset}`);
169
172
  state.workspaces.forEach((workspace, workspaceIndex) => {
170
173
  const rootIndex = workspaceIndex;
171
174
  const level = `workspace:${workspace.id}`;
@@ -178,7 +181,7 @@ function renderSidebar(state, height, width, p) {
178
181
  }
179
182
  });
180
183
  menuLines.push("");
181
- menuLines.push(`${p.muted} OPERATIONS${p.reset}`);
184
+ menuLines.push(`${p.muted} ${tr(state, "OPERATIONS")}${p.reset}`);
182
185
  const operations = data.navigation.filter((item) => item.section === "operations");
183
186
  operations.forEach((item, index) => {
184
187
  const rootIndex = state.workspaces.length + index;
@@ -186,9 +189,9 @@ function renderSidebar(state, height, width, p) {
186
189
  });
187
190
  const activeWorkspace = state.workspaces.find((item) => item.id === state.target.workspaceId);
188
191
  const footer = [
189
- `${p.muted} ACTIVE TARGET${p.reset}`,
192
+ `${p.muted} ${tr(state, "ACTIVE TARGET")}${p.reset}`,
190
193
  ` ${p.brand}${activeWorkspace?.icon || "◆"}${p.reset} ${truncate(activeWorkspace?.name || state.target.workspaceId, width - 5)}`,
191
- `${p.muted} ACTIVE CONVERSATION${p.reset}`,
194
+ `${p.muted} ${tr(state, "ACTIVE CONVERSATION")}${p.reset}`,
192
195
  ` ${p.cyan}↳${p.reset} ${truncate(state.lastConversation, width - 5)}`,
193
196
  `${p.green} ● local · ${state.adapterKind === "mock" ? "demo" : "Newmark core"}${p.reset}`
194
197
  ];
@@ -207,7 +210,7 @@ function conversationContext(state, p, scope) {
207
210
  const workspace = state.workspaces.find((item) => item.id === state.target.workspaceId);
208
211
  return [
209
212
  `${p.muted}Workspace / ${workspace?.name || state.target.workspaceId} / Conversation${p.reset}`,
210
- `${p.cyan}↳${p.reset} ${p.bold}${state.lastConversation}${p.reset} ${p.muted}· ${scope}${p.reset}`,
213
+ `${p.cyan}↳${p.reset} ${p.bold}${state.lastConversation}${p.reset} ${p.muted}· ${tr(state, scope)}${p.reset}`,
211
214
  `${p.muted}${"─".repeat(42)}${p.reset}`,
212
215
  ""
213
216
  ];
@@ -305,33 +308,66 @@ function buildEventRows(run, width, p, extraGuides = [], state = {}) {
305
308
  const rows = [];
306
309
  const ordered = orderedWorkEvents(run, extraGuides);
307
310
  const displayImages = runDisplayImages(run);
308
- const append = (prefix, content) => {
309
- const prefixWidth = visibleLength(prefix);
310
- wrapText(content, Math.max(1, width - prefixWidth)).forEach((row, index) => {
311
- rows.push(`${index === 0 ? prefix : " ".repeat(prefixWidth)}${row}`);
312
- });
311
+ const focusables = focusableEventsForRun(run);
312
+ const focusByEvent = new Map();
313
+ focusables.forEach((event, index) => focusByEvent.set(event, { key: buildEventKey(event, index), index }));
314
+ const eventFocus = !!(state.conversationHistoryFocus && state.historyEventFocus);
315
+ const focusedIndex = Number(state.historyEventIndex) || 0;
316
+ state.historyEventFocusLine = -1;
317
+ const zh = resolveLanguage(state) === "zh";
318
+ const labelFor = (event) => {
319
+ const type = String(event?.type || "").toLowerCase();
320
+ if (type === "tool_call") return "TOOL";
321
+ if (type === "tool_result") return "RESULT";
322
+ if (type === "thought" || type === "thought_result") return zh ? "思考" : "THOUGHT";
323
+ return "PROGRESS";
324
+ };
325
+ const nameFor = (event) => String(event?.toolName || "");
326
+ const contentFor = (event) => {
327
+ const type = String(event?.type || "").toLowerCase();
328
+ const content = String(event?.content || event?.toolArgs || "").trim();
329
+ if ((type === "thought" || type === "thought_result") && !content) return zh ? "正在思考…" : "Thinking…";
330
+ return content;
313
331
  };
332
+ let focusCursor = 0;
314
333
  for (const event of ordered) {
315
334
  if (isGuideEvent(event)) {
316
335
  rows.push(...guideEventRows(event, width, p));
317
336
  continue;
318
337
  }
319
338
  if (event.type === "final_response") continue;
320
- const content = String(event.content || event.toolArgs || "").trim();
321
- if (!content) continue;
339
+ const meta = focusByEvent.get(event);
340
+ const focusIndex = meta ? meta.index : focusCursor;
341
+ const key = meta ? meta.key : buildEventKey(event, focusIndex);
342
+ const focused = eventFocus && focusIndex === focusedIndex;
343
+ if (focused) state.historyEventFocusLine = rows.length;
344
+ const collapsed = !!(state.collapsedBuildEvents && state.collapsedBuildEvents.has(key));
345
+ const label = labelFor(event);
346
+ const name = nameFor(event);
347
+ const content = contentFor(event);
348
+ const chevron = collapsed ? "▸" : "▾";
349
+ const marker = focused ? "›" : " ";
350
+ if (collapsed || !content) {
351
+ const head = ` ${marker} ${chevron} ${label}${name ? ` ${name}` : ""}`;
352
+ rows.push(focused ? `${p.selected}${p.bold}${head}${p.reset}` : `${p.muted}${head}${p.reset}`);
353
+ } else {
354
+ const head = ` ${marker} ${chevron} ${label}${name ? ` ${name}` : ""} `;
355
+ const prefixWidth = visibleLength(head);
356
+ wrapText(content, Math.max(1, width - prefixWidth)).forEach((row, index) => {
357
+ const line = `${index === 0 ? head : " ".repeat(prefixWidth)}${row}`;
358
+ rows.push(focused ? `${p.selected}${p.bold}${line}${p.reset}` : `${p.muted}${line}${p.reset}`);
359
+ });
360
+ }
322
361
  if (event.type === "tool_call") {
323
- append(` TOOL ${event.toolName || "tool"} `, content);
324
362
  const result = ordered.find((candidate) => candidate.type === "tool_result" && candidate.displayImage && (
325
363
  event.toolCallId && candidate.toolCallId ? String(event.toolCallId) === String(candidate.toolCallId) : candidate.toolName === event.toolName
326
364
  ));
327
365
  if (result?.displayImage) rows.push(displayImageRow(result.displayImage, displayImages.indexOf(result.displayImage), state, p, width));
328
- } else if (event.type === "tool_result") {
329
- append(` RESULT ${event.toolName || "tool"} `, content);
330
- } else if (["text", "status", "response"].includes(event.type)) {
331
- append(" PROGRESS ", content);
332
366
  }
367
+ focusCursor = focusIndex + 1;
333
368
  }
334
- return rows.map((row) => `${p.muted}${row}${p.reset}`);
369
+ state.historyEventFocusCount = focusCursor;
370
+ return rows;
335
371
  }
336
372
 
337
373
  function collapsedGuideRows(run, width, p, extraGuides = []) {
@@ -424,6 +460,7 @@ function chatView(state, width, height, p) {
424
460
  const messageWidth = Math.max(8, detailWidth - roleColumnWidth);
425
461
  const historyRows = [];
426
462
  const historyBlockRanges = [];
463
+ state.historyEventFocusAbsoluteLine = -1;
427
464
  const workRuns = [...(state.snapshot.workRuns || [])]
428
465
  .sort((leftRun, rightRun) => Number(leftRun.sequence || 0) - Number(rightRun.sequence || 0));
429
466
  const workRunById = new Map(workRuns.map((run, index) => [String(run.runId || ""), { run, index }]));
@@ -480,6 +517,10 @@ function chatView(state, width, height, p) {
480
517
  )) || finalSummaryMessage(run);
481
518
  appendChatMessage(historyRows, assistantSummary, messageWidth, roleColumnWidth, p);
482
519
  historyBlockRanges[index] = { runId: run.runId, start, end };
520
+ if (state.conversationHistoryFocus && state.historyEventFocus
521
+ && Number.isInteger(state.historyEventFocusLine) && state.historyEventFocusLine >= 0) {
522
+ state.historyEventFocusAbsoluteLine = start + 1 + state.historyEventFocusLine;
523
+ }
483
524
  renderedRunIds.add(runId);
484
525
  };
485
526
  for (const message of state.messages) {
@@ -513,6 +554,19 @@ function chatView(state, width, height, p) {
513
554
  state.conversationScroll = Math.max(0, historyRows.length - historyEnd);
514
555
  }
515
556
  }
557
+ if (state.conversationHistoryFocus && state.historyEventFocus
558
+ && Number.isInteger(state.historyEventFocusAbsoluteLine) && state.historyEventFocusAbsoluteLine >= 0) {
559
+ const focusLine = state.historyEventFocusAbsoluteLine;
560
+ if (focusLine < historyStart) {
561
+ historyStart = focusLine;
562
+ historyEnd = Math.min(historyRows.length, historyStart + historyHeight);
563
+ state.conversationScroll = Math.max(0, historyRows.length - historyEnd);
564
+ } else if (focusLine >= historyEnd) {
565
+ historyEnd = Math.min(historyRows.length, focusLine + 1);
566
+ historyStart = Math.max(0, historyEnd - historyHeight);
567
+ state.conversationScroll = Math.max(0, historyRows.length - historyEnd);
568
+ }
569
+ }
516
570
  state.historyVisibleRunIds = historyBlockRanges
517
571
  .filter(Boolean)
518
572
  .filter((range) => range.end >= historyStart && range.start < historyEnd)
@@ -539,18 +593,19 @@ function planView(state, width, p) {
539
593
  const goal = state.snapshot.goal;
540
594
  const lines = [
541
595
  ...conversationContext(state, p, "Conversation plan"),
542
- `${p.bold}Linked Plan · rev ${state.snapshot.linkedPlan.revision}${p.reset} ${p.cyan}${planItems.filter((item) => item.status === "done").length} / ${planItems.length} steps${p.reset}`,
596
+ `${p.bold}${tr(state, "Linked Plan")} · rev ${state.snapshot.linkedPlan.revision}${p.reset} ${p.cyan}${planItems.filter((item) => item.status === "done").length} / ${planItems.length} steps${p.reset}`,
543
597
  "",
544
598
  ...planItems.map((item, i) => {
545
599
  const style = state.focusRegion === "content" && i === state.selected ? `${p.selected}${p.bold}` : "";
600
+ if (style) state.contentFocusLine = 6 + i;
546
601
  return `${style} ${statusIcon(item.status, p)} ${pad(item.text, Math.max(18, width - 28))} ${p.muted}${item.status}${p.reset}`;
547
602
  }),
548
603
  "",
549
604
  ...card([
550
- `${p.bold}Next handoff${p.reset}`,
605
+ `${p.bold}${tr(state, "Next handoff")}${p.reset}`,
551
606
  goal?.objective || "No active goal",
552
- `${p.muted}Plan and goal are loaded from the active Newmark conversation${p.reset}`
553
- ], width, p, "Linked goal")
607
+ `${p.muted}${tr(state, "Records come from the active Newmark conversation")}${p.reset}`
608
+ ], width, p, tr(state, "Linked goal"))
554
609
  ];
555
610
  return lines;
556
611
  }
@@ -559,9 +614,9 @@ function goalView(state, width, p) {
559
614
  const goal = state.snapshot.goal;
560
615
  return [
561
616
  ...conversationContext(state, p, "Conversation goal"),
562
- `${p.bold}Goal${p.reset} ${goal?.paused ? `${p.amber}paused${p.reset}` : `${p.green}active${p.reset}`}`,
617
+ `${p.bold}${tr(state, "Goal")}${p.reset} ${goal?.paused ? `${p.amber}paused${p.reset}` : `${p.green}active${p.reset}`}`,
563
618
  "",
564
- `${p.brand}${p.bold}${goal?.objective || "No active goal"}${p.reset}`,
619
+ `${p.brand}${p.bold}${goal?.objective || tr(state, "No active goal")}${p.reset}`,
565
620
  "",
566
621
  `Rounds ${goal?.goalRounds || 0}`,
567
622
  `Verified ${goal?.verified ? "yes" : "no"}`,
@@ -576,11 +631,12 @@ function agentsView(state, width, p) {
576
631
  const messageWidth = Math.max(8, width - roleColumnWidth - 4);
577
632
  const rows = [
578
633
  ...conversationContext(state, p, "Conversation subagents"),
579
- `${p.bold}Subagents${p.reset} ${p.muted}Parallel work with isolated context${p.reset}`,
634
+ `${p.bold}${tr(state, "Subagents")}${p.reset} ${p.muted}${tr(state, "Parallel work with isolated context")}${p.reset}`,
580
635
  ""
581
636
  ];
582
637
  state.snapshot.subagents.forEach((agent, i) => {
583
638
  const style = state.focusRegion === "content" && i === state.selected ? `${p.selected}` : "";
639
+ if (style) state.contentFocusLine = rows.length;
584
640
  const expanded = state.agentHistoryExpandedId === String(agent.id || agent.displayName || "");
585
641
  rows.push(
586
642
  `${style} ${expanded ? "▾" : "▸"} ${statusIcon(agent.status, p)} ${p.bold}${pad(agent.displayName, 18)}${p.reset} ${pad(agent.task, Math.max(12, width - 49))} ${p.muted}${agent.status}${p.reset}`,
@@ -597,11 +653,11 @@ function agentsView(state, width, p) {
597
653
  }, messageWidth, roleColumnWidth, p);
598
654
  });
599
655
  if (!messages.length) {
600
- historyRows.push(`${" ".repeat(roleColumnWidth)}${p.muted}No messages recorded yet.${p.reset}`);
656
+ historyRows.push(`${" ".repeat(roleColumnWidth)}${p.muted}${tr(state, "No messages recorded yet.")}${p.reset}`);
601
657
  historyRows.push("");
602
658
  }
603
659
  if (agent.result) {
604
- historyRows.push(`${p.brand}${p.bold}RESULT${p.reset}`);
660
+ historyRows.push(`${p.brand}${p.bold}${tr(state, "RESULT")}${p.reset}`);
605
661
  wrapText(String(agent.result), messageWidth).forEach((row) => {
606
662
  historyRows.push(`${" ".repeat(roleColumnWidth)}${row}`);
607
663
  });
@@ -626,16 +682,18 @@ function modelView(state, width, p) {
626
682
  || (selection.providerId === current.providerId && selection.modelId === current.modelId));
627
683
  return [
628
684
  ...conversationContext(state, p, "Model and reasoning effort"),
629
- `${p.bold}Reasoning effort${p.reset} ${p.muted}Shared GUI/TUI request tier · ←/→ changes section${p.reset}`,
685
+ `${p.bold}${tr(state, "Reasoning effort")}${p.reset} ${p.muted}${tr(state, "Shared GUI/TUI request tier · ←/→ changes section")}${p.reset}`,
630
686
  ...INTELLIGENCE_TIERS.map((tier, index) => {
631
687
  const style = state.focusRegion === "content" && state.contentColumn === 0 && index === state.selected ? `${p.selected}${p.bold}` : "";
688
+ if (style) state.contentFocusLine = 5 + index;
632
689
  return `${style} ${tier === currentTier ? `${p.cyan}●${p.reset}` : "○"} ${tier}${p.reset}`;
633
690
  }),
634
691
  "",
635
- `${p.bold}Deployment${p.reset} ${p.muted}Used by this conversation, including its Plan and Subagents${p.reset}`,
692
+ `${p.bold}${tr(state, "Deployment")}${p.reset} ${p.muted}${tr(state, "Used by this conversation, including its Plan and Subagents")}${p.reset}`,
636
693
  "",
637
694
  ...options.flatMap((option, index) => {
638
695
  const style = state.focusRegion === "content" && state.contentColumn === 1 && index === state.selected ? `${p.selected}${p.bold}` : "";
696
+ if (style) state.contentFocusLine = 14 + index * 2;
639
697
  const marker = isCurrent(option.selection) ? `${p.cyan}●${p.reset}` : "○";
640
698
  return [
641
699
  `${style} ${marker} ${pad(option.label, Math.max(18, Math.min(32, width - 24)))} ${p.muted}${option.provider}${p.reset}`,
@@ -643,7 +701,7 @@ function modelView(state, width, p) {
643
701
  ];
644
702
  }),
645
703
  "",
646
- `${p.muted}Enter applies the focused tier or deployment. Effort persists globally; deployments remain per conversation.${p.reset}`
704
+ `${p.muted}${tr(state, "Enter applies the focused tier or deployment. Effort persists globally; deployments remain per conversation.")}${p.reset}`
647
705
  ];
648
706
  }
649
707
 
@@ -651,7 +709,7 @@ function flowBarView(state, width, p) {
651
709
  const workflow = state.currentFlow;
652
710
  return [
653
711
  ...conversationContext(state, p, "Flow bar"),
654
- `${p.bold}Flow Bar${p.reset} ${p.green}● tracked${p.reset}`,
712
+ `${p.bold}${tr(state, "Flow Bar")}${p.reset} ${p.green}● tracked${p.reset}`,
655
713
  "",
656
714
  `${p.brand}${p.bold}${workflow?.name || "No workflow selected"}${p.reset}`,
657
715
  `${p.muted}Mode ${state.snapshot.mode} · current task ${Number(workflow?.pc || 0) + 1} / ${workflow?.components?.length || 0}${p.reset}`,
@@ -665,11 +723,12 @@ function flowBarView(state, width, p) {
665
723
  function flowListView(state, width, p) {
666
724
  return [
667
725
  ...conversationContext(state, p, "Flow list"),
668
- `${p.bold}Flow List${p.reset} ${p.muted}Available workflows for this tracked conversation${p.reset}`,
726
+ `${p.bold}${tr(state, "Flow List")}${p.reset} ${p.muted}Available workflows for this tracked conversation${p.reset}`,
669
727
  "",
670
728
  ...state.flows.map((name, index) => {
671
729
  const selected = state.currentFlow?.name === name;
672
730
  const style = state.focusRegion === "content" && index === state.selected ? `${p.selected}${p.bold}` : "";
731
+ if (style) state.contentFocusLine = 6 + index;
673
732
  return `${style} ${selected ? `${p.cyan}●${p.reset}` : "○"} ${pad(name, Math.max(18, width - 8))}`;
674
733
  })
675
734
  ];
@@ -679,10 +738,11 @@ function flowTaskView(state, width, p) {
679
738
  const components = state.currentFlow?.components || [];
680
739
  return [
681
740
  ...conversationContext(state, p, "Flow task"),
682
- `${p.bold}Flow Task${p.reset} ${p.muted}${state.currentFlow?.name || "No workflow"}${p.reset}`,
741
+ `${p.bold}${tr(state, "Flow Task")}${p.reset} ${p.muted}${state.currentFlow?.name || "No workflow"}${p.reset}`,
683
742
  "",
684
743
  ...components.flatMap((component, index) => {
685
744
  const style = state.focusRegion === "content" && index === state.selected ? `${p.selected}${p.bold}` : "";
745
+ if (style) state.contentFocusLine = 6 + index * 2;
686
746
  const mode = component.type === "logic" ? "LOGIC" : String(component.mode || "BUILD").toUpperCase();
687
747
  return [
688
748
  `${style} ${String(component.id).padStart(2)} ${pad(mode, 7)} ${truncate(component.prompt, Math.max(18, width - 16))}`,
@@ -695,11 +755,12 @@ function flowTaskView(state, width, p) {
695
755
  function toolsView(state, width, p) {
696
756
  const real = state.adapterKind !== "mock";
697
757
  return [
698
- `${p.bold}Tools & connectors${p.reset} ${p.muted}Enter toggles ${real ? "Newmark native-tool config" : "access in this demo"}${p.reset}`,
758
+ `${p.bold}${tr(state, "Tools & connectors")}${p.reset} ${p.muted}Enter toggles ${real ? "Newmark native-tool config" : "access in this demo"}${p.reset}`,
699
759
  "",
700
760
  `${p.muted} STATE TOOL SOURCE CALLS${p.reset}`,
701
761
  ...state.tools.map((tool, i) => {
702
762
  const style = state.focusRegion === "content" && i === state.selected ? `${p.selected}${p.bold}` : "";
763
+ if (style) state.contentFocusLine = 3 + i;
703
764
  const stateLabel = tool.enabled ? `${p.green} ON ${p.reset}` : `${p.muted}OFF ${p.reset}`;
704
765
  return `${style} ${stateLabel} ${pad(tool.name, 20)} ${pad(tool.group, Math.max(12, width - 48))} ${String(tool.calls).padStart(5)}${p.reset}`;
705
766
  }),
@@ -707,18 +768,20 @@ function toolsView(state, width, p) {
707
768
  ...card([
708
769
  real ? "Tool availability is loaded from the active Newmark config." : "Tool calls are represented for interaction design only.",
709
770
  real ? "Agent prompts may execute enabled tools under Newmark's existing approval policy." : "No shell, file, browser, network, or connector action is available here."
710
- ], width, p, real ? "Live runtime" : "Safety boundary")
771
+ ], width, p, real ? tr(state, "Live runtime") : tr(state, "Safety boundary"))
711
772
  ];
712
773
  }
713
774
 
714
775
  function automationView(state, width, p) {
776
+ if (state.focusRegion === "content" && state.selected === 0) state.contentFocusLine = 2;
715
777
  return [
716
- `${p.bold}Automations${p.reset} ${p.muted}Schedules bound to explicit workspaces${p.reset}`,
778
+ `${p.bold}${tr(state, "Automations")}${p.reset} ${p.muted}Schedules bound to explicit workspaces${p.reset}`,
717
779
  "",
718
- `${state.focusRegion === "content" && state.selected === 0 ? `${p.selected}${p.bold}` : ""} ${p.cyan}[+] New automation${p.reset} ${p.muted}Create a schedule for this workspace${p.reset}`,
780
+ `${state.focusRegion === "content" && state.selected === 0 ? `${p.selected}${p.bold}` : ""} ${p.cyan}${tr(state, "[+] New automation")}${p.reset} ${p.muted}Create a schedule for this workspace${p.reset}`,
719
781
  "",
720
782
  ...state.automations.flatMap((job, i) => {
721
783
  const style = state.focusRegion === "content" && i + 1 === state.selected ? `${p.selected}${p.bold}` : "";
784
+ if (style) state.contentFocusLine = 4 + i * 2;
722
785
  return [
723
786
  `${style} ${job.enabled ? `${p.green}● ACTIVE${p.reset}` : `${p.muted}○ PAUSED${p.reset}`} ${pad(job.name, 24)} ${p.muted}${job.schedule}${p.reset}`,
724
787
  ` Last run: ${job.last}`
@@ -730,17 +793,19 @@ function automationView(state, width, p) {
730
793
  }
731
794
 
732
795
  function workflowView(state, width, p) {
796
+ if (state.focusRegion === "content" && state.selected === 0) state.contentFocusLine = 2;
733
797
  const rows = [
734
- `${p.bold}WorkFlow${p.reset} ${p.muted}Manage Flow definitions stored under ~/.Newmark/Flow${p.reset}`,
798
+ `${p.bold}${tr(state, "WorkFlow")}${p.reset} ${p.muted}Manage Flow definitions stored under ~/.Newmark/Flow${p.reset}`,
735
799
  "",
736
- `${state.focusRegion === "content" && state.selected === 0 ? `${p.selected}${p.bold}` : ""} ${p.cyan}[+] New workflow${p.reset} ${p.muted}Create a valid first dialog component${p.reset}`,
800
+ `${state.focusRegion === "content" && state.selected === 0 ? `${p.selected}${p.bold}` : ""} ${p.cyan}${tr(state, "[+] New workflow")}${p.reset} ${p.muted}Create a valid first dialog component${p.reset}`,
737
801
  ""
738
802
  ];
739
803
  if (!state.flows.length) {
740
- rows.push(`${p.muted}No workflows configured.${p.reset}`);
804
+ rows.push(`${p.muted}${tr(state, "No workflows configured.")}${p.reset}`);
741
805
  }
742
806
  state.flows.forEach((name, index) => {
743
807
  const selected = state.focusRegion === "content" && state.selected === index + 1;
808
+ if (selected) state.contentFocusLine = rows.length;
744
809
  const workflow = state.workflowDetails[name] || { components: [] };
745
810
  const expanded = state.workflowExpandedName === name;
746
811
  rows.push(`${selected ? `${p.selected}${p.bold}` : ""} ${expanded ? "▾" : "▸"} ${pad(name, Math.max(18, width - 28))} ${p.muted}${workflow.components.length} component(s)${p.reset}`);
@@ -777,9 +842,9 @@ function memoryView(state, width, p) {
777
842
  : [`${p.muted}— none —${p.reset}`]
778
843
  )
779
844
  ];
780
- const tagRows = relationColumn("Tags", tags, 0);
781
- const selectedRows = relationColumn("Selected tag", detail.tag ? [detail.tag] : [], 1);
782
- const childRows = relationColumn("Child tags", children, 2);
845
+ const tagRows = relationColumn(tr(state, "Tags"), tags, 0);
846
+ const selectedRows = relationColumn(tr(state, "Selected tag"), detail.tag ? [detail.tag] : [], 1);
847
+ const childRows = relationColumn(tr(state, "Child tags"), children, 2);
783
848
  const relationRows = Array.from(
784
849
  { length: Math.max(tagRows.length, selectedRows.length, childRows.length) },
785
850
  (_, row) => `${pad(tagRows[row] || "", relationWidth)} ${p.muted}│${p.reset} ${pad(selectedRows[row] || "", relationWidth)} ${p.muted}│${p.reset} ${childRows[row] || ""}`
@@ -795,12 +860,12 @@ function memoryView(state, width, p) {
795
860
  });
796
861
  const contentLines = detail.content.split(/\r?\n/).filter(Boolean).slice(0, 7);
797
862
  const components = [
798
- `${p.bold}Memory components${p.reset}`,
799
- ...(componentNames.length ? componentNames : [`${p.muted}No memory components${p.reset}`])
863
+ `${p.bold}${tr(state, "Memory components")}${p.reset}`,
864
+ ...(componentNames.length ? componentNames : [`${p.muted}${tr(state, "No memory components")}${p.reset}`])
800
865
  ];
801
866
  const preview = [
802
- `${p.bold}Core memory${p.reset}`,
803
- `${p.bold}${detail.component?.name || "No component selected"}${p.reset}`,
867
+ `${p.bold}${tr(state, "Core memory")}${p.reset}`,
868
+ `${p.bold}${detail.component?.name || tr(state, "No component selected")}${p.reset}`,
804
869
  `${p.muted}${detail.component?.description || ""}${p.reset}`,
805
870
  `${p.muted}Parents: ${detail.node.parents.join(", ") || "none"} · Children: ${children.join(", ") || "none"}${p.reset}`,
806
871
  ...contentLines,
@@ -810,9 +875,16 @@ function memoryView(state, width, p) {
810
875
  { length: Math.max(components.length, preview.length) },
811
876
  (_, row) => `${pad(components[row] || "", componentWidth)} ${p.muted}│${p.reset} ${preview[row] || ""}`
812
877
  );
878
+ if (state.focusRegion === "content") {
879
+ const focusColumn = Math.min(3, Math.max(0, state.contentColumn));
880
+ const focusIndex = state.memoryColumnIndices[focusColumn] || 0;
881
+ state.contentFocusLine = focusColumn === 3
882
+ ? 3 + relationRows.length + 2 + focusIndex
883
+ : 4 + focusIndex;
884
+ }
813
885
  return [
814
- `${p.bold}Memory Lab${p.reset} ${state.memorySearchActive ? p.selected : p.muted}/${p.reset} ${state.memorySearchQuery || `${p.muted}search tags${p.reset}`}${state.memorySearchActive ? `${p.cyan}▏${p.reset}` : ""}`,
815
- `${p.muted}/ search · Esc clear · ←/→ columns · ↑/↓ select · Enter follow/select/open overview${p.reset}`,
886
+ `${p.bold}${tr(state, "Memory Lab")}${p.reset} ${state.memorySearchActive ? p.selected : p.muted}/${p.reset} ${state.memorySearchQuery || `${p.muted}search tags${p.reset}`}${state.memorySearchActive ? `${p.cyan}▏${p.reset}` : ""}`,
887
+ `${p.muted}/ search · O overview · Esc clear · ←/→ columns · ↑/↓ select · Enter follow/select${p.reset}`,
816
888
  `${p.muted}${"─".repeat(Math.max(8, width - 1))}${p.reset}`,
817
889
  ...relationRows,
818
890
  "",
@@ -826,13 +898,14 @@ function settingsView(state, width, p) {
826
898
  const categories = SETTINGS_CATEGORIES;
827
899
  const leftWidth = Math.min(22, Math.max(18, Math.floor(width * 0.24)));
828
900
  const categoryRows = [
829
- `${p.bold}Settings${p.reset}`,
830
- `${p.muted}Categories${p.reset}`,
901
+ `${p.bold}${tr(state, "Settings")}${p.reset}`,
902
+ `${p.muted}${tr(state, "Categories")}${p.reset}`,
831
903
  "",
832
904
  ...categories.map(({ id, icon, label }, index) => {
833
905
  const focused = state.focusRegion === "content" && state.contentColumn === 0 && state.settingsCategoryIndex === index;
906
+ if (focused) state.contentFocusLine = 3 + index;
834
907
  const active = state.settingsTab === id;
835
- return `${focused ? `${p.selected}${p.bold}` : active ? p.bold : ""}${active ? `${p.cyan}>${p.reset}` : " "} ${icon} ${label}${p.reset}`;
908
+ return `${focused ? `${p.selected}${p.bold}` : active ? p.bold : ""}${active ? `${p.cyan}>${p.reset}` : " "} ${icon} ${tr(state, label)}${p.reset}`;
836
909
  }),
837
910
  "",
838
911
  `${p.muted}→ enter section${p.reset}`,
@@ -851,6 +924,7 @@ function settingsView(state, width, p) {
851
924
  `${p.muted} STATE PROVIDER PROTOCOL KEY MODELS${p.reset}`,
852
925
  ...state.providers.map((provider, index) => {
853
926
  const style = state.focusRegion === "content" && state.contentColumn === 1 && index === state.selected ? `${p.selected}${p.bold}` : "";
927
+ if (style) state.contentFocusLine = 5 + index;
854
928
  const enabled = provider.enabled ? `${p.green} ON ${p.reset}` : `${p.muted}OFF ${p.reset}`;
855
929
  const key = provider.has_api_key ? `${p.green}set${p.reset}` : `${p.amber}none${p.reset}`;
856
930
  return `${style} ${enabled} ${pad(provider.name, 22)} ${pad(provider.protocol, 14)} ${key} ${String(provider.models.length).padStart(3)}${p.reset}`;
@@ -866,6 +940,7 @@ function settingsView(state, width, p) {
866
940
  `${p.muted} STATE MODEL PROVIDER VALIDATION CTX${p.reset}`,
867
941
  ...models.map((entry, index) => {
868
942
  const style = state.focusRegion === "content" && state.contentColumn === 1 && index === state.selected ? `${p.selected}${p.bold}` : "";
943
+ if (style) state.contentFocusLine = 5 + index;
869
944
  const enabled = entry.model.enabled !== false && entry.provider.enabled;
870
945
  const stateLabel = enabled ? `${p.green} ON ${p.reset}` : `${p.muted}OFF ${p.reset}`;
871
946
  const validation = entry.model.validation || {};
@@ -881,6 +956,7 @@ function settingsView(state, width, p) {
881
956
  `${p.muted} STATE TOOL GROUP CALLS${p.reset}`,
882
957
  ...state.tools.map((tool, index) => {
883
958
  const style = state.focusRegion === "content" && state.contentColumn === 1 && index === state.selected ? `${p.selected}${p.bold}` : "";
959
+ if (style) state.contentFocusLine = 5 + index;
884
960
  return `${style} ${tool.enabled ? `${p.green} ON ${p.reset}` : `${p.muted}OFF ${p.reset}`} ${pad(tool.name, 23)} ${pad(tool.group, 17)} ${String(tool.calls).padStart(5)}${p.reset}`;
885
961
  }),
886
962
  "",
@@ -892,6 +968,7 @@ function settingsView(state, width, p) {
892
968
  ...header,
893
969
  ...rows.flatMap((row, i) => {
894
970
  const style = state.focusRegion === "content" && state.contentColumn === 1 && i === state.selected ? `${p.selected}` : "";
971
+ if (style) state.contentFocusLine = 4 + i * 2;
895
972
  return [
896
973
  `${style} ${p.bold}${pad(row.label, 27)}${p.reset} ${p.cyan}${displaySettingValue(row.value)}${p.reset}${row.extension ? ` ${p.amber}TUI EXTENSION${p.reset}` : ""}`,
897
974
  ` ${p.muted}${settingDescription(state.settingsTab, row.key)}${p.reset}`
@@ -988,16 +1065,16 @@ function settingsActionHint(tab) {
988
1065
 
989
1066
  function helpView(state, width, p) {
990
1067
  return [
991
- `${p.brand}${p.bold}SHORTCUT GUIDE${p.reset} ${p.muted}Context-aware TUI controls${p.reset}`,
1068
+ `${p.brand}${p.bold}${tr(state, "SHORTCUT GUIDE")}${p.reset} ${p.muted}Context-aware TUI controls${p.reset}`,
992
1069
  `${p.muted}${"─".repeat(Math.max(20, width - 1))}${p.reset}`,
993
1070
  "",
994
- `${p.bold}Navigation${p.reset}`,
1071
+ `${p.bold}${tr(state, "Navigation")}${p.reset}`,
995
1072
  " ↑ / ↓ Move within lists; long menus auto-scroll with the cursor",
996
1073
  " ← / → Move between menu levels and content columns",
997
1074
  " Enter Expand workspace, open view, select conversation, or confirm",
998
1075
  " Tab From editor: conversation selection; from content: left menu",
999
1076
  "",
1000
- `${p.bold}Conversation editing${p.reset}`,
1077
+ `${p.bold}${tr(state, "Conversation editing")}${p.reset}`,
1001
1078
  " Enter Edit selected conversation / send current draft",
1002
1079
  " Enter mode Settings → General → Input mode chooses default Guide or Next",
1003
1080
  " Shift+Enter Insert a newline; the input viewport always reserves two rows",
@@ -1008,17 +1085,17 @@ function helpView(state, width, p) {
1008
1085
  " Flow Requires ↑ / ↓ + Enter workflow selection before editing",
1009
1086
  " T Pin / unpin selected conversation; cursor follows its ID",
1010
1087
  "",
1011
- `${p.bold}Running work${p.reset}`,
1088
+ `${p.bold}${tr(state, "Running work")}${p.reset}`,
1012
1089
  " \\ — / — Rotating left marker means that conversation is running",
1013
1090
  " Esc Request graceful stop for the focused running conversation",
1014
1091
  " Esc again Force-stop that same conversation",
1015
1092
  " Switching focus never interrupts background conversations",
1016
1093
  "",
1017
- `${p.bold}Operation content${p.reset}`,
1094
+ `${p.bold}${tr(state, "Operation content")}${p.reset}`,
1018
1095
  " Automations First row creates a persisted workspace-bound automation",
1019
1096
  " WorkFlow Flat Operation item; content manages and creates Flow files",
1020
1097
  "",
1021
- `${p.bold}Global${p.reset}`,
1098
+ `${p.bold}${tr(state, "Global")}${p.reset}`,
1022
1099
  " Ctrl+K Command palette",
1023
1100
  " N New conversation",
1024
1101
  " T Toggle theme outside conversation selection",
@@ -1044,6 +1121,7 @@ function renderContent(state, width, height, p) {
1044
1121
  settings: settingsView,
1045
1122
  help: helpView
1046
1123
  };
1124
+ state.contentFocusLine = -1;
1047
1125
  return views[state.view](state, width, p);
1048
1126
  }
1049
1127
 
@@ -1158,9 +1236,22 @@ function render(state, columns = process.stdout.columns || 100, rows = process.s
1158
1236
  const bodyHeight = height - 3;
1159
1237
  const sidebar = sidebarWidth ? renderSidebar(state, bodyHeight, sidebarWidth, p) : [];
1160
1238
  const content = renderContent(state, contentWidth, bodyHeight, p);
1239
+ let contentLines = content;
1240
+ if (state.view !== "chat") {
1241
+ const maximumScroll = Math.max(0, content.length - bodyHeight);
1242
+ let scroll = Math.max(0, Math.min(maximumScroll, Number(state.contentScroll) || 0));
1243
+ const focusLine = Number(state.contentFocusLine) || -1;
1244
+ if (focusLine >= 0) {
1245
+ if (focusLine < scroll) scroll = focusLine;
1246
+ else if (focusLine >= scroll + bodyHeight) scroll = focusLine - bodyHeight + 1;
1247
+ }
1248
+ scroll = Math.max(0, Math.min(maximumScroll, scroll));
1249
+ state.contentScroll = scroll;
1250
+ contentLines = content.slice(scroll, scroll + bodyHeight);
1251
+ }
1161
1252
  const body = Array.from({ length: bodyHeight }, (_, i) => {
1162
1253
  const left = sidebarWidth ? `${pad(sidebar[i] || "", sidebarWidth)}${p.muted}│${p.reset} ` : "";
1163
- return `${left}${pad(content[i] || "", contentWidth)}`;
1254
+ return `${left}${pad(contentLines[i] || "", contentWidth)}`;
1164
1255
  });
1165
1256
  const activeNav = data.navigation.find((item) => item.id === state.view);
1166
1257
  const compactContext = ["chat", "plan", "agents"].includes(state.view)