newmark-agent 0.4.0 → 0.4.3

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 = []) {
@@ -394,19 +430,28 @@ function chatView(state, width, height, p) {
394
430
  `${p.bold}Conversations${p.reset}`,
395
431
  `${p.muted}N new${p.reset}`,
396
432
  "",
397
- ...state.snapshot.conversations.map((item, i) => {
398
- const style = state.focusRegion === "content" && i === state.selected && !state.inputMode ? `${p.selected}${p.bold}` : "";
399
- const running = state.runningConversationKeys?.has(`${state.target.workspaceId}::${item.id}`);
400
- const marker = running
401
- ? `${p.cyan}${["\\", "—", "/", "—"][state.tick % 4]}${p.reset}`
402
- : item.title === state.lastConversation
403
- ? `${p.cyan}›${p.reset}`
404
- : item.pinned
405
- ? `${p.amber}◆${p.reset}`
406
- : "·";
407
- const updated = String(item.updatedAt || "").slice(11, 16) || "—";
408
- return `${style}${marker} ${pad(item.title, 19)} ${p.muted}${updated}${p.reset}`;
409
- })
433
+ ...(() => {
434
+ const conversationRows = state.snapshot.conversations.map((item, i) => {
435
+ const style = state.focusRegion === "content" && i === state.selected && !state.inputMode ? `${p.selected}${p.bold}` : "";
436
+ const running = state.runningConversationKeys?.has(`${state.target.workspaceId}::${item.id}`);
437
+ const marker = running
438
+ ? `${p.cyan}${["\\", "—", "/", "—"][state.tick % 4]}${p.reset}`
439
+ : item.title === state.lastConversation
440
+ ? `${p.cyan}›${p.reset}`
441
+ : item.pinned
442
+ ? `${p.amber}◆${p.reset}`
443
+ : "·";
444
+ const updated = String(item.updatedAt || "").slice(11, 16) || "—";
445
+ return `${style}${marker} ${pad(item.title, 19)} ${p.muted}${updated}${p.reset}`;
446
+ });
447
+ const listViewport = Math.max(1, height - 3);
448
+ const listMaxScroll = Math.max(0, conversationRows.length - listViewport);
449
+ let listScroll = Math.max(0, Math.min(listMaxScroll, Number(state.conversationListScroll) || 0));
450
+ if (state.selected < listScroll) listScroll = state.selected;
451
+ else if (state.selected >= listScroll + listViewport) listScroll = state.selected - listViewport + 1;
452
+ state.conversationListScroll = Math.max(0, Math.min(listMaxScroll, listScroll));
453
+ return conversationRows.slice(state.conversationListScroll, state.conversationListScroll + listViewport);
454
+ })()
410
455
  ] : [];
411
456
  const preview = state.snapshot.conversations[state.selected] || state.snapshot.conversations[0] || {
412
457
  id: state.target.conversationId,
@@ -424,6 +469,7 @@ function chatView(state, width, height, p) {
424
469
  const messageWidth = Math.max(8, detailWidth - roleColumnWidth);
425
470
  const historyRows = [];
426
471
  const historyBlockRanges = [];
472
+ state.historyEventFocusAbsoluteLine = -1;
427
473
  const workRuns = [...(state.snapshot.workRuns || [])]
428
474
  .sort((leftRun, rightRun) => Number(leftRun.sequence || 0) - Number(rightRun.sequence || 0));
429
475
  const workRunById = new Map(workRuns.map((run, index) => [String(run.runId || ""), { run, index }]));
@@ -480,6 +526,10 @@ function chatView(state, width, height, p) {
480
526
  )) || finalSummaryMessage(run);
481
527
  appendChatMessage(historyRows, assistantSummary, messageWidth, roleColumnWidth, p);
482
528
  historyBlockRanges[index] = { runId: run.runId, start, end };
529
+ if (state.conversationHistoryFocus && state.historyEventFocus
530
+ && Number.isInteger(state.historyEventFocusLine) && state.historyEventFocusLine >= 0) {
531
+ state.historyEventFocusAbsoluteLine = start + 1 + state.historyEventFocusLine;
532
+ }
483
533
  renderedRunIds.add(runId);
484
534
  };
485
535
  for (const message of state.messages) {
@@ -513,6 +563,19 @@ function chatView(state, width, height, p) {
513
563
  state.conversationScroll = Math.max(0, historyRows.length - historyEnd);
514
564
  }
515
565
  }
566
+ if (state.conversationHistoryFocus && state.historyEventFocus
567
+ && Number.isInteger(state.historyEventFocusAbsoluteLine) && state.historyEventFocusAbsoluteLine >= 0) {
568
+ const focusLine = state.historyEventFocusAbsoluteLine;
569
+ if (focusLine < historyStart) {
570
+ historyStart = focusLine;
571
+ historyEnd = Math.min(historyRows.length, historyStart + historyHeight);
572
+ state.conversationScroll = Math.max(0, historyRows.length - historyEnd);
573
+ } else if (focusLine >= historyEnd) {
574
+ historyEnd = Math.min(historyRows.length, focusLine + 1);
575
+ historyStart = Math.max(0, historyEnd - historyHeight);
576
+ state.conversationScroll = Math.max(0, historyRows.length - historyEnd);
577
+ }
578
+ }
516
579
  state.historyVisibleRunIds = historyBlockRanges
517
580
  .filter(Boolean)
518
581
  .filter((range) => range.end >= historyStart && range.start < historyEnd)
@@ -539,18 +602,19 @@ function planView(state, width, p) {
539
602
  const goal = state.snapshot.goal;
540
603
  const lines = [
541
604
  ...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}`,
605
+ `${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
606
  "",
544
607
  ...planItems.map((item, i) => {
545
608
  const style = state.focusRegion === "content" && i === state.selected ? `${p.selected}${p.bold}` : "";
609
+ if (style) state.contentFocusLine = 6 + i;
546
610
  return `${style} ${statusIcon(item.status, p)} ${pad(item.text, Math.max(18, width - 28))} ${p.muted}${item.status}${p.reset}`;
547
611
  }),
548
612
  "",
549
613
  ...card([
550
- `${p.bold}Next handoff${p.reset}`,
614
+ `${p.bold}${tr(state, "Next handoff")}${p.reset}`,
551
615
  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")
616
+ `${p.muted}${tr(state, "Records come from the active Newmark conversation")}${p.reset}`
617
+ ], width, p, tr(state, "Linked goal"))
554
618
  ];
555
619
  return lines;
556
620
  }
@@ -559,9 +623,9 @@ function goalView(state, width, p) {
559
623
  const goal = state.snapshot.goal;
560
624
  return [
561
625
  ...conversationContext(state, p, "Conversation goal"),
562
- `${p.bold}Goal${p.reset} ${goal?.paused ? `${p.amber}paused${p.reset}` : `${p.green}active${p.reset}`}`,
626
+ `${p.bold}${tr(state, "Goal")}${p.reset} ${goal?.paused ? `${p.amber}paused${p.reset}` : `${p.green}active${p.reset}`}`,
563
627
  "",
564
- `${p.brand}${p.bold}${goal?.objective || "No active goal"}${p.reset}`,
628
+ `${p.brand}${p.bold}${goal?.objective || tr(state, "No active goal")}${p.reset}`,
565
629
  "",
566
630
  `Rounds ${goal?.goalRounds || 0}`,
567
631
  `Verified ${goal?.verified ? "yes" : "no"}`,
@@ -576,11 +640,12 @@ function agentsView(state, width, p) {
576
640
  const messageWidth = Math.max(8, width - roleColumnWidth - 4);
577
641
  const rows = [
578
642
  ...conversationContext(state, p, "Conversation subagents"),
579
- `${p.bold}Subagents${p.reset} ${p.muted}Parallel work with isolated context${p.reset}`,
643
+ `${p.bold}${tr(state, "Subagents")}${p.reset} ${p.muted}${tr(state, "Parallel work with isolated context")}${p.reset}`,
580
644
  ""
581
645
  ];
582
646
  state.snapshot.subagents.forEach((agent, i) => {
583
647
  const style = state.focusRegion === "content" && i === state.selected ? `${p.selected}` : "";
648
+ if (style) state.contentFocusLine = rows.length;
584
649
  const expanded = state.agentHistoryExpandedId === String(agent.id || agent.displayName || "");
585
650
  rows.push(
586
651
  `${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 +662,11 @@ function agentsView(state, width, p) {
597
662
  }, messageWidth, roleColumnWidth, p);
598
663
  });
599
664
  if (!messages.length) {
600
- historyRows.push(`${" ".repeat(roleColumnWidth)}${p.muted}No messages recorded yet.${p.reset}`);
665
+ historyRows.push(`${" ".repeat(roleColumnWidth)}${p.muted}${tr(state, "No messages recorded yet.")}${p.reset}`);
601
666
  historyRows.push("");
602
667
  }
603
668
  if (agent.result) {
604
- historyRows.push(`${p.brand}${p.bold}RESULT${p.reset}`);
669
+ historyRows.push(`${p.brand}${p.bold}${tr(state, "RESULT")}${p.reset}`);
605
670
  wrapText(String(agent.result), messageWidth).forEach((row) => {
606
671
  historyRows.push(`${" ".repeat(roleColumnWidth)}${row}`);
607
672
  });
@@ -626,16 +691,18 @@ function modelView(state, width, p) {
626
691
  || (selection.providerId === current.providerId && selection.modelId === current.modelId));
627
692
  return [
628
693
  ...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}`,
694
+ `${p.bold}${tr(state, "Reasoning effort")}${p.reset} ${p.muted}${tr(state, "Shared GUI/TUI request tier · ←/→ changes section")}${p.reset}`,
630
695
  ...INTELLIGENCE_TIERS.map((tier, index) => {
631
696
  const style = state.focusRegion === "content" && state.contentColumn === 0 && index === state.selected ? `${p.selected}${p.bold}` : "";
697
+ if (style) state.contentFocusLine = 5 + index;
632
698
  return `${style} ${tier === currentTier ? `${p.cyan}●${p.reset}` : "○"} ${tier}${p.reset}`;
633
699
  }),
634
700
  "",
635
- `${p.bold}Deployment${p.reset} ${p.muted}Used by this conversation, including its Plan and Subagents${p.reset}`,
701
+ `${p.bold}${tr(state, "Deployment")}${p.reset} ${p.muted}${tr(state, "Used by this conversation, including its Plan and Subagents")}${p.reset}`,
636
702
  "",
637
703
  ...options.flatMap((option, index) => {
638
704
  const style = state.focusRegion === "content" && state.contentColumn === 1 && index === state.selected ? `${p.selected}${p.bold}` : "";
705
+ if (style) state.contentFocusLine = 14 + index * 2;
639
706
  const marker = isCurrent(option.selection) ? `${p.cyan}●${p.reset}` : "○";
640
707
  return [
641
708
  `${style} ${marker} ${pad(option.label, Math.max(18, Math.min(32, width - 24)))} ${p.muted}${option.provider}${p.reset}`,
@@ -643,7 +710,7 @@ function modelView(state, width, p) {
643
710
  ];
644
711
  }),
645
712
  "",
646
- `${p.muted}Enter applies the focused tier or deployment. Effort persists globally; deployments remain per conversation.${p.reset}`
713
+ `${p.muted}${tr(state, "Enter applies the focused tier or deployment. Effort persists globally; deployments remain per conversation.")}${p.reset}`
647
714
  ];
648
715
  }
649
716
 
@@ -651,7 +718,7 @@ function flowBarView(state, width, p) {
651
718
  const workflow = state.currentFlow;
652
719
  return [
653
720
  ...conversationContext(state, p, "Flow bar"),
654
- `${p.bold}Flow Bar${p.reset} ${p.green}● tracked${p.reset}`,
721
+ `${p.bold}${tr(state, "Flow Bar")}${p.reset} ${p.green}● tracked${p.reset}`,
655
722
  "",
656
723
  `${p.brand}${p.bold}${workflow?.name || "No workflow selected"}${p.reset}`,
657
724
  `${p.muted}Mode ${state.snapshot.mode} · current task ${Number(workflow?.pc || 0) + 1} / ${workflow?.components?.length || 0}${p.reset}`,
@@ -665,11 +732,12 @@ function flowBarView(state, width, p) {
665
732
  function flowListView(state, width, p) {
666
733
  return [
667
734
  ...conversationContext(state, p, "Flow list"),
668
- `${p.bold}Flow List${p.reset} ${p.muted}Available workflows for this tracked conversation${p.reset}`,
735
+ `${p.bold}${tr(state, "Flow List")}${p.reset} ${p.muted}Available workflows for this tracked conversation${p.reset}`,
669
736
  "",
670
737
  ...state.flows.map((name, index) => {
671
738
  const selected = state.currentFlow?.name === name;
672
739
  const style = state.focusRegion === "content" && index === state.selected ? `${p.selected}${p.bold}` : "";
740
+ if (style) state.contentFocusLine = 6 + index;
673
741
  return `${style} ${selected ? `${p.cyan}●${p.reset}` : "○"} ${pad(name, Math.max(18, width - 8))}`;
674
742
  })
675
743
  ];
@@ -677,29 +745,30 @@ function flowListView(state, width, p) {
677
745
 
678
746
  function flowTaskView(state, width, p) {
679
747
  const components = state.currentFlow?.components || [];
680
- return [
748
+ const rows = [
681
749
  ...conversationContext(state, p, "Flow task"),
682
- `${p.bold}Flow Task${p.reset} ${p.muted}${state.currentFlow?.name || "No workflow"}${p.reset}`,
683
- "",
684
- ...components.flatMap((component, index) => {
685
- const style = state.focusRegion === "content" && index === state.selected ? `${p.selected}${p.bold}` : "";
686
- const mode = component.type === "logic" ? "LOGIC" : String(component.mode || "BUILD").toUpperCase();
687
- return [
688
- `${style} ${String(component.id).padStart(2)} ${pad(mode, 7)} ${truncate(component.prompt, Math.max(18, width - 16))}`,
689
- component.type === "logic" ? ` ${p.muted}true → ${component.goto_true} · false → ${component.goto_false}${p.reset}` : ""
690
- ].filter(Boolean);
691
- })
750
+ `${p.bold}${tr(state, "Flow Task")}${p.reset} ${p.muted}${state.currentFlow?.name || "No workflow"}${p.reset}`,
751
+ ""
692
752
  ];
753
+ components.forEach((component, index) => {
754
+ const style = state.focusRegion === "content" && index === state.selected ? `${p.selected}${p.bold}` : "";
755
+ if (style) state.contentFocusLine = rows.length;
756
+ const mode = component.type === "logic" ? "LOGIC" : String(component.mode || "BUILD").toUpperCase();
757
+ rows.push(`${style} ${String(component.id).padStart(2)} ${pad(mode, 7)} ${truncate(component.prompt, Math.max(18, width - 16))}`);
758
+ if (component.type === "logic") rows.push(` ${p.muted}true → ${component.goto_true} · false → ${component.goto_false}${p.reset}`);
759
+ });
760
+ return rows;
693
761
  }
694
762
 
695
763
  function toolsView(state, width, p) {
696
764
  const real = state.adapterKind !== "mock";
697
765
  return [
698
- `${p.bold}Tools & connectors${p.reset} ${p.muted}Enter toggles ${real ? "Newmark native-tool config" : "access in this demo"}${p.reset}`,
766
+ `${p.bold}${tr(state, "Tools & connectors")}${p.reset} ${p.muted}Enter toggles ${real ? "Newmark native-tool config" : "access in this demo"}${p.reset}`,
699
767
  "",
700
768
  `${p.muted} STATE TOOL SOURCE CALLS${p.reset}`,
701
769
  ...state.tools.map((tool, i) => {
702
770
  const style = state.focusRegion === "content" && i === state.selected ? `${p.selected}${p.bold}` : "";
771
+ if (style) state.contentFocusLine = 3 + i;
703
772
  const stateLabel = tool.enabled ? `${p.green} ON ${p.reset}` : `${p.muted}OFF ${p.reset}`;
704
773
  return `${style} ${stateLabel} ${pad(tool.name, 20)} ${pad(tool.group, Math.max(12, width - 48))} ${String(tool.calls).padStart(5)}${p.reset}`;
705
774
  }),
@@ -707,18 +776,20 @@ function toolsView(state, width, p) {
707
776
  ...card([
708
777
  real ? "Tool availability is loaded from the active Newmark config." : "Tool calls are represented for interaction design only.",
709
778
  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")
779
+ ], width, p, real ? tr(state, "Live runtime") : tr(state, "Safety boundary"))
711
780
  ];
712
781
  }
713
782
 
714
783
  function automationView(state, width, p) {
784
+ if (state.focusRegion === "content" && state.selected === 0) state.contentFocusLine = 2;
715
785
  return [
716
- `${p.bold}Automations${p.reset} ${p.muted}Schedules bound to explicit workspaces${p.reset}`,
786
+ `${p.bold}${tr(state, "Automations")}${p.reset} ${p.muted}Schedules bound to explicit workspaces${p.reset}`,
717
787
  "",
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}`,
788
+ `${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
789
  "",
720
790
  ...state.automations.flatMap((job, i) => {
721
791
  const style = state.focusRegion === "content" && i + 1 === state.selected ? `${p.selected}${p.bold}` : "";
792
+ if (style) state.contentFocusLine = 4 + i * 2;
722
793
  return [
723
794
  `${style} ${job.enabled ? `${p.green}● ACTIVE${p.reset}` : `${p.muted}○ PAUSED${p.reset}`} ${pad(job.name, 24)} ${p.muted}${job.schedule}${p.reset}`,
724
795
  ` Last run: ${job.last}`
@@ -730,17 +801,19 @@ function automationView(state, width, p) {
730
801
  }
731
802
 
732
803
  function workflowView(state, width, p) {
804
+ if (state.focusRegion === "content" && state.selected === 0) state.contentFocusLine = 2;
733
805
  const rows = [
734
- `${p.bold}WorkFlow${p.reset} ${p.muted}Manage Flow definitions stored under ~/.Newmark/Flow${p.reset}`,
806
+ `${p.bold}${tr(state, "WorkFlow")}${p.reset} ${p.muted}Manage Flow definitions stored under ~/.Newmark/Flow${p.reset}`,
735
807
  "",
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}`,
808
+ `${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
809
  ""
738
810
  ];
739
811
  if (!state.flows.length) {
740
- rows.push(`${p.muted}No workflows configured.${p.reset}`);
812
+ rows.push(`${p.muted}${tr(state, "No workflows configured.")}${p.reset}`);
741
813
  }
742
814
  state.flows.forEach((name, index) => {
743
815
  const selected = state.focusRegion === "content" && state.selected === index + 1;
816
+ if (selected) state.contentFocusLine = rows.length;
744
817
  const workflow = state.workflowDetails[name] || { components: [] };
745
818
  const expanded = state.workflowExpandedName === name;
746
819
  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 +850,9 @@ function memoryView(state, width, p) {
777
850
  : [`${p.muted}— none —${p.reset}`]
778
851
  )
779
852
  ];
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);
853
+ const tagRows = relationColumn(tr(state, "Tags"), tags, 0);
854
+ const selectedRows = relationColumn(tr(state, "Selected tag"), detail.tag ? [detail.tag] : [], 1);
855
+ const childRows = relationColumn(tr(state, "Child tags"), children, 2);
783
856
  const relationRows = Array.from(
784
857
  { length: Math.max(tagRows.length, selectedRows.length, childRows.length) },
785
858
  (_, row) => `${pad(tagRows[row] || "", relationWidth)} ${p.muted}│${p.reset} ${pad(selectedRows[row] || "", relationWidth)} ${p.muted}│${p.reset} ${childRows[row] || ""}`
@@ -795,12 +868,12 @@ function memoryView(state, width, p) {
795
868
  });
796
869
  const contentLines = detail.content.split(/\r?\n/).filter(Boolean).slice(0, 7);
797
870
  const components = [
798
- `${p.bold}Memory components${p.reset}`,
799
- ...(componentNames.length ? componentNames : [`${p.muted}No memory components${p.reset}`])
871
+ `${p.bold}${tr(state, "Memory components")}${p.reset}`,
872
+ ...(componentNames.length ? componentNames : [`${p.muted}${tr(state, "No memory components")}${p.reset}`])
800
873
  ];
801
874
  const preview = [
802
- `${p.bold}Core memory${p.reset}`,
803
- `${p.bold}${detail.component?.name || "No component selected"}${p.reset}`,
875
+ `${p.bold}${tr(state, "Core memory")}${p.reset}`,
876
+ `${p.bold}${detail.component?.name || tr(state, "No component selected")}${p.reset}`,
804
877
  `${p.muted}${detail.component?.description || ""}${p.reset}`,
805
878
  `${p.muted}Parents: ${detail.node.parents.join(", ") || "none"} · Children: ${children.join(", ") || "none"}${p.reset}`,
806
879
  ...contentLines,
@@ -810,9 +883,16 @@ function memoryView(state, width, p) {
810
883
  { length: Math.max(components.length, preview.length) },
811
884
  (_, row) => `${pad(components[row] || "", componentWidth)} ${p.muted}│${p.reset} ${preview[row] || ""}`
812
885
  );
886
+ if (state.focusRegion === "content") {
887
+ const focusColumn = Math.min(3, Math.max(0, state.contentColumn));
888
+ const focusIndex = state.memoryColumnIndices[focusColumn] || 0;
889
+ state.contentFocusLine = focusColumn === 3
890
+ ? 3 + relationRows.length + 2 + focusIndex
891
+ : 4 + focusIndex;
892
+ }
813
893
  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}`,
894
+ `${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}` : ""}`,
895
+ `${p.muted}/ search · O overview · Esc clear · ←/→ columns · ↑/↓ select · Enter follow/select${p.reset}`,
816
896
  `${p.muted}${"─".repeat(Math.max(8, width - 1))}${p.reset}`,
817
897
  ...relationRows,
818
898
  "",
@@ -826,13 +906,14 @@ function settingsView(state, width, p) {
826
906
  const categories = SETTINGS_CATEGORIES;
827
907
  const leftWidth = Math.min(22, Math.max(18, Math.floor(width * 0.24)));
828
908
  const categoryRows = [
829
- `${p.bold}Settings${p.reset}`,
830
- `${p.muted}Categories${p.reset}`,
909
+ `${p.bold}${tr(state, "Settings")}${p.reset}`,
910
+ `${p.muted}${tr(state, "Categories")}${p.reset}`,
831
911
  "",
832
912
  ...categories.map(({ id, icon, label }, index) => {
833
913
  const focused = state.focusRegion === "content" && state.contentColumn === 0 && state.settingsCategoryIndex === index;
914
+ if (focused) state.contentFocusLine = 3 + index;
834
915
  const active = state.settingsTab === id;
835
- return `${focused ? `${p.selected}${p.bold}` : active ? p.bold : ""}${active ? `${p.cyan}>${p.reset}` : " "} ${icon} ${label}${p.reset}`;
916
+ return `${focused ? `${p.selected}${p.bold}` : active ? p.bold : ""}${active ? `${p.cyan}>${p.reset}` : " "} ${icon} ${tr(state, label)}${p.reset}`;
836
917
  }),
837
918
  "",
838
919
  `${p.muted}→ enter section${p.reset}`,
@@ -851,6 +932,7 @@ function settingsView(state, width, p) {
851
932
  `${p.muted} STATE PROVIDER PROTOCOL KEY MODELS${p.reset}`,
852
933
  ...state.providers.map((provider, index) => {
853
934
  const style = state.focusRegion === "content" && state.contentColumn === 1 && index === state.selected ? `${p.selected}${p.bold}` : "";
935
+ if (style) state.contentFocusLine = 5 + index;
854
936
  const enabled = provider.enabled ? `${p.green} ON ${p.reset}` : `${p.muted}OFF ${p.reset}`;
855
937
  const key = provider.has_api_key ? `${p.green}set${p.reset}` : `${p.amber}none${p.reset}`;
856
938
  return `${style} ${enabled} ${pad(provider.name, 22)} ${pad(provider.protocol, 14)} ${key} ${String(provider.models.length).padStart(3)}${p.reset}`;
@@ -866,6 +948,7 @@ function settingsView(state, width, p) {
866
948
  `${p.muted} STATE MODEL PROVIDER VALIDATION CTX${p.reset}`,
867
949
  ...models.map((entry, index) => {
868
950
  const style = state.focusRegion === "content" && state.contentColumn === 1 && index === state.selected ? `${p.selected}${p.bold}` : "";
951
+ if (style) state.contentFocusLine = 5 + index;
869
952
  const enabled = entry.model.enabled !== false && entry.provider.enabled;
870
953
  const stateLabel = enabled ? `${p.green} ON ${p.reset}` : `${p.muted}OFF ${p.reset}`;
871
954
  const validation = entry.model.validation || {};
@@ -881,6 +964,7 @@ function settingsView(state, width, p) {
881
964
  `${p.muted} STATE TOOL GROUP CALLS${p.reset}`,
882
965
  ...state.tools.map((tool, index) => {
883
966
  const style = state.focusRegion === "content" && state.contentColumn === 1 && index === state.selected ? `${p.selected}${p.bold}` : "";
967
+ if (style) state.contentFocusLine = 5 + index;
884
968
  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
969
  }),
886
970
  "",
@@ -892,6 +976,7 @@ function settingsView(state, width, p) {
892
976
  ...header,
893
977
  ...rows.flatMap((row, i) => {
894
978
  const style = state.focusRegion === "content" && state.contentColumn === 1 && i === state.selected ? `${p.selected}` : "";
979
+ if (style) state.contentFocusLine = 4 + i * 2;
895
980
  return [
896
981
  `${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
982
  ` ${p.muted}${settingDescription(state.settingsTab, row.key)}${p.reset}`
@@ -988,16 +1073,16 @@ function settingsActionHint(tab) {
988
1073
 
989
1074
  function helpView(state, width, p) {
990
1075
  return [
991
- `${p.brand}${p.bold}SHORTCUT GUIDE${p.reset} ${p.muted}Context-aware TUI controls${p.reset}`,
1076
+ `${p.brand}${p.bold}${tr(state, "SHORTCUT GUIDE")}${p.reset} ${p.muted}Context-aware TUI controls${p.reset}`,
992
1077
  `${p.muted}${"─".repeat(Math.max(20, width - 1))}${p.reset}`,
993
1078
  "",
994
- `${p.bold}Navigation${p.reset}`,
1079
+ `${p.bold}${tr(state, "Navigation")}${p.reset}`,
995
1080
  " ↑ / ↓ Move within lists; long menus auto-scroll with the cursor",
996
1081
  " ← / → Move between menu levels and content columns",
997
1082
  " Enter Expand workspace, open view, select conversation, or confirm",
998
1083
  " Tab From editor: conversation selection; from content: left menu",
999
1084
  "",
1000
- `${p.bold}Conversation editing${p.reset}`,
1085
+ `${p.bold}${tr(state, "Conversation editing")}${p.reset}`,
1001
1086
  " Enter Edit selected conversation / send current draft",
1002
1087
  " Enter mode Settings → General → Input mode chooses default Guide or Next",
1003
1088
  " Shift+Enter Insert a newline; the input viewport always reserves two rows",
@@ -1008,17 +1093,17 @@ function helpView(state, width, p) {
1008
1093
  " Flow Requires ↑ / ↓ + Enter workflow selection before editing",
1009
1094
  " T Pin / unpin selected conversation; cursor follows its ID",
1010
1095
  "",
1011
- `${p.bold}Running work${p.reset}`,
1096
+ `${p.bold}${tr(state, "Running work")}${p.reset}`,
1012
1097
  " \\ — / — Rotating left marker means that conversation is running",
1013
1098
  " Esc Request graceful stop for the focused running conversation",
1014
1099
  " Esc again Force-stop that same conversation",
1015
1100
  " Switching focus never interrupts background conversations",
1016
1101
  "",
1017
- `${p.bold}Operation content${p.reset}`,
1102
+ `${p.bold}${tr(state, "Operation content")}${p.reset}`,
1018
1103
  " Automations First row creates a persisted workspace-bound automation",
1019
1104
  " WorkFlow Flat Operation item; content manages and creates Flow files",
1020
1105
  "",
1021
- `${p.bold}Global${p.reset}`,
1106
+ `${p.bold}${tr(state, "Global")}${p.reset}`,
1022
1107
  " Ctrl+K Command palette",
1023
1108
  " N New conversation",
1024
1109
  " T Toggle theme outside conversation selection",
@@ -1044,6 +1129,7 @@ function renderContent(state, width, height, p) {
1044
1129
  settings: settingsView,
1045
1130
  help: helpView
1046
1131
  };
1132
+ state.contentFocusLine = -1;
1047
1133
  return views[state.view](state, width, p);
1048
1134
  }
1049
1135
 
@@ -1065,11 +1151,19 @@ function overlayLines(state, width, p) {
1065
1151
  }
1066
1152
  if (state.overlay === "palette") {
1067
1153
  const commands = filteredCommands(state);
1154
+ const paletteViewport = Math.min(7, Math.max(1, commands.length));
1155
+ const paletteMaxScroll = Math.max(0, commands.length - paletteViewport);
1156
+ let paletteScroll = Math.max(0, Math.min(paletteMaxScroll, Number(state.paletteScroll) || 0));
1157
+ if (state.paletteIndex < paletteScroll) paletteScroll = state.paletteIndex;
1158
+ else if (state.paletteIndex >= paletteScroll + paletteViewport) paletteScroll = state.paletteIndex - paletteViewport + 1;
1159
+ state.paletteScroll = Math.max(0, Math.min(paletteMaxScroll, paletteScroll));
1160
+ const visibleCommands = commands.slice(state.paletteScroll, state.paletteScroll + paletteViewport);
1068
1161
  const rows = [
1069
1162
  `${p.bold}>${p.reset} ${state.paletteQuery}${p.cyan}▏${p.reset}`,
1070
1163
  `${p.muted}${"─".repeat(Math.max(8, Math.min(64, width - 10)))}${p.reset}`,
1071
- ...(commands.length ? commands.slice(0, 7).map((command, i) => {
1072
- const style = i === state.paletteIndex ? `${p.selected}${p.bold}` : "";
1164
+ ...(visibleCommands.length ? visibleCommands.map((command, i) => {
1165
+ const commandIndex = state.paletteScroll + i;
1166
+ const style = commandIndex === state.paletteIndex ? `${p.selected}${p.bold}` : "";
1073
1167
  return `${style} ${pad(command.label, Math.max(18, Math.min(52, width - 18)))} ${p.muted}${command.hint}${p.reset}`;
1074
1168
  }) : [`${p.muted} No matching commands${p.reset}`]),
1075
1169
  "",
@@ -1078,14 +1172,22 @@ function overlayLines(state, width, p) {
1078
1172
  return card(rows, Math.min(72, width - 4), p, "Command palette");
1079
1173
  }
1080
1174
  if (state.overlay === "flow-select") {
1175
+ const flowViewport = Math.min(7, Math.max(1, state.flows.length));
1176
+ const flowMaxScroll = Math.max(0, state.flows.length - flowViewport);
1177
+ let flowScroll = Math.max(0, Math.min(flowMaxScroll, Number(state.flowSelectionScroll) || 0));
1178
+ if (state.flowSelectionIndex < flowScroll) flowScroll = state.flowSelectionIndex;
1179
+ else if (state.flowSelectionIndex >= flowScroll + flowViewport) flowScroll = state.flowSelectionIndex - flowViewport + 1;
1180
+ state.flowSelectionScroll = Math.max(0, Math.min(flowMaxScroll, flowScroll));
1181
+ const visibleFlows = state.flows.slice(state.flowSelectionScroll, state.flowSelectionScroll + flowViewport);
1081
1182
  return card([
1082
1183
  `${p.bold}Flow mode requires a workflow${p.reset}`,
1083
1184
  `${p.muted}This selection is bound to ${state.lastConversation}.${p.reset}`,
1084
1185
  "",
1085
- ...(state.flows.length
1086
- ? state.flows.map((name, index) => {
1087
- const style = index === state.flowSelectionIndex ? `${p.selected}${p.bold}` : "";
1088
- return `${style} ${index === state.flowSelectionIndex ? "›" : " "} ${name}${p.reset}`;
1186
+ ...(visibleFlows.length
1187
+ ? visibleFlows.map((name, i) => {
1188
+ const flowIndex = state.flowSelectionScroll + i;
1189
+ const style = flowIndex === state.flowSelectionIndex ? `${p.selected}${p.bold}` : "";
1190
+ return `${style} ${flowIndex === state.flowSelectionIndex ? "›" : " "} ${name}${p.reset}`;
1089
1191
  })
1090
1192
  : [`${p.amber}No workflows are configured in ~/.Newmark/Flow.${p.reset}`]),
1091
1193
  "",
@@ -1158,9 +1260,22 @@ function render(state, columns = process.stdout.columns || 100, rows = process.s
1158
1260
  const bodyHeight = height - 3;
1159
1261
  const sidebar = sidebarWidth ? renderSidebar(state, bodyHeight, sidebarWidth, p) : [];
1160
1262
  const content = renderContent(state, contentWidth, bodyHeight, p);
1263
+ let contentLines = content;
1264
+ if (state.view !== "chat") {
1265
+ const maximumScroll = Math.max(0, content.length - bodyHeight);
1266
+ let scroll = Math.max(0, Math.min(maximumScroll, Number(state.contentScroll) || 0));
1267
+ const focusLine = Number(state.contentFocusLine) || -1;
1268
+ if (focusLine >= 0) {
1269
+ if (focusLine < scroll) scroll = focusLine;
1270
+ else if (focusLine >= scroll + bodyHeight) scroll = focusLine - bodyHeight + 1;
1271
+ }
1272
+ scroll = Math.max(0, Math.min(maximumScroll, scroll));
1273
+ state.contentScroll = scroll;
1274
+ contentLines = content.slice(scroll, scroll + bodyHeight);
1275
+ }
1161
1276
  const body = Array.from({ length: bodyHeight }, (_, i) => {
1162
1277
  const left = sidebarWidth ? `${pad(sidebar[i] || "", sidebarWidth)}${p.muted}│${p.reset} ` : "";
1163
- return `${left}${pad(content[i] || "", contentWidth)}`;
1278
+ return `${left}${pad(contentLines[i] || "", contentWidth)}`;
1164
1279
  });
1165
1280
  const activeNav = data.navigation.find((item) => item.id === state.view);
1166
1281
  const compactContext = ["chat", "plan", "agents"].includes(state.view)