refactor(i18n): route claude pane labels through the catalog (T-464)

Migrate the hardcoded user-facing strings across builtin.claude — composer,
conversation cards/segments, permission + AskUserQuestion prompts, task dock,
meta-sidebar (activity/config/team/roster), session/model pickers — to
ClideSettings.i18n.string/.interpolated, English kept as the placeholder;
builtin.claude_en_us.json extended to cover them. Context threaded into the
context-free render helpers (toolInputBody chain, _ConversationTurn) the same
way the mono family already is. No en_US behaviour change (D-21).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 21:19:19 +02:00
co-authored by Claude Opus 4.8
parent 93aa9bc332
commit 16fe71cb3f
23 changed files with 870 additions and 193 deletions
+16 -2
View File
@@ -35,7 +35,12 @@ class ClaudeBanner extends StatelessWidget {
children: [
const ClideSvgView.asset('assets/logo/logo.svg', width: 60, height: 60),
const SizedBox(height: 18),
ClideText('Claude', fontSize: clideFontDialogTitle, color: claudeAccent, fontWeight: FontWeight.w500),
ClideText(
ClideSettings.i18n.string(context, 'banner.title', namespace: 'builtin.claude', placeholder: 'Claude'),
fontSize: clideFontDialogTitle,
color: claudeAccent,
fontWeight: FontWeight.w500,
),
const SizedBox(height: 2),
ClideText(role, fontSize: clideFontSmall, muted: true, fontFamily: ClideSettings.fonts.monoOf(context)),
const SizedBox(height: 16),
@@ -45,7 +50,16 @@ class ClaudeBanner extends StatelessWidget {
ClideText(statusLine!, fontSize: clideFontSmall, muted: true, fontFamily: ClideSettings.fonts.monoOf(context)),
],
const SizedBox(height: 16),
const ClideText('Warming up — your conversation will appear here.', fontSize: clideFontSmall, muted: true),
ClideText(
ClideSettings.i18n.string(
context,
'banner.warmingUp',
namespace: 'builtin.claude',
placeholder: 'Warming up — your conversation will appear here.',
),
fontSize: clideFontSmall,
muted: true,
),
],
),
);
+32 -9
View File
@@ -44,7 +44,7 @@ class ClaudeComposer extends StatefulWidget {
super.key,
required this.onSubmit,
this.enabled = true,
this.hint = 'Message Claude… (Enter to send · Shift+Enter for newline)',
this.hint,
this.pasteResolver,
this.slashCommandsResolver,
this.onInterrupt,
@@ -63,7 +63,11 @@ class ClaudeComposer extends StatefulWidget {
final void Function(String text) onSubmit;
final bool enabled;
final String hint;
/// Placeholder shown when the composer is empty. Null falls back to the
/// i18n catalog default (`composer.hint`), resolved at render so it honours
/// the live locale (D-21).
final String? hint;
/// Optional override of paste handling: returns the attachments on the
/// clipboard (files / images), or an empty list to fall back to the
@@ -417,6 +421,14 @@ class _ClaudeComposerState extends State<ClaudeComposer> {
final theme = ClideSettings.theme.of(context).surface;
final hasText = _controller.text.isNotEmpty;
final fg = widget.enabled ? theme.globalForeground : theme.globalTextMuted;
final hint =
widget.hint ??
ClideSettings.i18n.string(
context,
'composer.hint',
namespace: 'builtin.claude',
placeholder: 'Message Claude… (Enter to send · Shift+Enter for newline)',
);
return Padding(
padding: const EdgeInsets.fromLTRB(10, 6, 10, 10),
@@ -444,10 +456,15 @@ class _ClaudeComposerState extends State<ClaudeComposer> {
const RunningIndicator(),
const Spacer(),
ClideButton(
label: 'Stop ⎋',
label: ClideSettings.i18n.string(context, 'composer.stop', namespace: 'builtin.claude', placeholder: 'Stop ⎋'),
variant: ClideButtonVariant.primary,
onPressed: widget.onInterrupt,
semanticHint: 'Interrupt the running turn (Escape)',
semanticHint: ClideSettings.i18n.string(
context,
'composer.stop.hint',
namespace: 'builtin.claude',
placeholder: 'Interrupt the running turn (Escape)',
),
),
],
),
@@ -455,14 +472,14 @@ class _ClaudeComposerState extends State<ClaudeComposer> {
if (_attachments.isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Wrap(spacing: 6, runSpacing: 6, children: [for (final a in _attachments) _chip(theme, a)]),
child: Wrap(spacing: 6, runSpacing: 6, children: [for (final a in _attachments) _chip(context, theme, a)]),
),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Semantics(
label: widget.hint,
label: hint,
textField: true,
child: Shortcuts(
shortcuts: const {
@@ -486,7 +503,7 @@ class _ClaudeComposerState extends State<ClaudeComposer> {
},
child: Stack(
children: [
if (!hasText) Positioned(left: 0, top: 0, right: 0, child: ClideText(widget.hint, muted: true, fontSize: clideFontBody)),
if (!hasText) Positioned(left: 0, top: 0, right: 0, child: ClideText(hint, muted: true, fontSize: clideFontBody)),
EditableText(
controller: _controller,
focusNode: _focus,
@@ -521,7 +538,7 @@ class _ClaudeComposerState extends State<ClaudeComposer> {
/// One attachment chip: a thumbnail (images) or file icon (other types),
/// the filename, and a remove × that cancels the attachment before send.
Widget _chip(SurfaceTokens theme, ComposerAttachment a) {
Widget _chip(BuildContext context, SurfaceTokens theme, ComposerAttachment a) {
return Container(
constraints: const BoxConstraints(maxWidth: 220),
decoration: BoxDecoration(
@@ -541,7 +558,13 @@ class _ClaudeComposerState extends State<ClaudeComposer> {
const SizedBox(width: 4),
Semantics(
button: true,
label: 'Remove ${a.fileName}',
label: ClideSettings.i18n.interpolated(
context,
'composer.removeAttachment',
namespace: 'builtin.claude',
placeholder: 'Remove ${a.fileName}',
replacers: [I18nReplacer(from: '{name}', replace: a.fileName)],
),
child: GestureDetector(
key: ValueKey('composer-remove-${a.path}'),
onTap: () => _removeAttachment(a),
+10 -2
View File
@@ -833,7 +833,9 @@ class _ClaudePaneState extends State<ClaudePane> {
},
);
} else {
body = const Center(child: ClideText('starting…', muted: true));
body = Center(
child: ClideText(ClideSettings.i18n.string(context, 'pane.starting', namespace: 'builtin.claude', placeholder: 'starting…'), muted: true),
);
}
final content = widget.showChrome ? ClidePaneChrome(title: title, subtitle: _error ?? _statusLine, child: body) : body;
@@ -856,7 +858,13 @@ class _ModeBadge extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Semantics(
label: 'permission mode: ${permissionModeLabel(mode)}',
label: ClideSettings.i18n.interpolated(
context,
'pane.modeBadge.semantics',
namespace: 'builtin.claude',
placeholder: 'permission mode: ${permissionModeLabel(mode)}',
replacers: [I18nReplacer(from: '{mode}', replace: permissionModeLabel(mode))],
),
excludeSemantics: true,
child: ClideText(
permissionModeLabel(mode),
+57 -7
View File
@@ -33,13 +33,28 @@ class _ClaudeTaskDockState extends State<ClaudeTaskDock> {
final done = tasks.where((t) => t.status == TaskStatus.completed).length;
final inProgress = tasks.where((t) => t.status == TaskStatus.inProgress);
final current = inProgress.isEmpty ? null : inProgress.first.text;
final summary = '${tasks.length} task${tasks.length == 1 ? '' : 's'} · $done done';
final taskWord = tasks.length == 1
? ClideSettings.i18n.string(context, 'taskDock.task.singular', namespace: 'builtin.claude', placeholder: 'task')
: ClideSettings.i18n.string(context, 'taskDock.task.plural', namespace: 'builtin.claude', placeholder: 'tasks');
final summary = ClideSettings.i18n.interpolated(
context,
'taskDock.summary',
namespace: 'builtin.claude',
placeholder: '${tasks.length} $taskWord · $done done',
replacers: [
I18nReplacer(from: '{count}', replace: '${tasks.length}'),
I18nReplacer(from: '{tasks}', replace: taskWord),
I18nReplacer(from: '{done}', replace: '$done'),
],
);
return Padding(
padding: const EdgeInsets.fromLTRB(10, 0, 10, 6),
child: ClideTappable(
onTap: () => setState(() => _expanded = !_expanded),
tooltip: _expanded ? 'Collapse tasks' : 'Expand tasks',
tooltip: _expanded
? ClideSettings.i18n.string(context, 'taskDock.collapse', namespace: 'builtin.claude', placeholder: 'Collapse tasks')
: ClideSettings.i18n.string(context, 'taskDock.expand', namespace: 'builtin.claude', placeholder: 'Expand tasks'),
builder: (context, hovered, focused) => Container(
decoration: BoxDecoration(
color: (hovered || focused) ? tokens.listItemHoverBackground : tokens.listItemBackground,
@@ -53,7 +68,21 @@ class _ClaudeTaskDockState extends State<ClaudeTaskDock> {
// (its inner text is announced via the label, so exclude it).
Semantics(
button: true,
label: 'Claude task list, $summary, ${_expanded ? 'expanded' : 'collapsed'}',
label: ClideSettings.i18n.interpolated(
context,
'taskDock.semantics',
namespace: 'builtin.claude',
placeholder: 'Claude task list, $summary, ${_expanded ? 'expanded' : 'collapsed'}',
replacers: [
I18nReplacer(from: '{summary}', replace: summary),
I18nReplacer(
from: '{state}',
replace: _expanded
? ClideSettings.i18n.string(context, 'taskDock.state.expanded', namespace: 'builtin.claude', placeholder: 'expanded')
: ClideSettings.i18n.string(context, 'taskDock.state.collapsed', namespace: 'builtin.claude', placeholder: 'collapsed'),
),
],
),
excludeSemantics: true,
child: _summaryRow(tokens, summary, current),
),
@@ -91,14 +120,35 @@ class _ClaudeTaskDockState extends State<ClaudeTaskDock> {
Widget _taskRow(SurfaceTokens tokens, TaskItem t) {
final (String glyph, Color color, String word) = switch (t.status) {
TaskStatus.completed => ('check-circle', tokens.statusSuccess, 'done'),
TaskStatus.inProgress => ('circle-half', tokens.globalFocus, 'in progress'),
TaskStatus.pending => ('circle', tokens.globalTextMuted, 'pending'),
TaskStatus.completed => (
'check-circle',
tokens.statusSuccess,
ClideSettings.i18n.string(context, 'taskDock.status.done', namespace: 'builtin.claude', placeholder: 'done'),
),
TaskStatus.inProgress => (
'circle-half',
tokens.globalFocus,
ClideSettings.i18n.string(context, 'taskDock.status.inProgress', namespace: 'builtin.claude', placeholder: 'in progress'),
),
TaskStatus.pending => (
'circle',
tokens.globalTextMuted,
ClideSettings.i18n.string(context, 'taskDock.status.pending', namespace: 'builtin.claude', placeholder: 'pending'),
),
};
return Padding(
padding: const EdgeInsets.only(top: 4),
child: Semantics(
label: '${t.text}, $word',
label: ClideSettings.i18n.interpolated(
context,
'taskDock.row.semantics',
namespace: 'builtin.claude',
placeholder: '${t.text}, $word',
replacers: [
I18nReplacer(from: '{text}', replace: t.text),
I18nReplacer(from: '{status}', replace: word),
],
),
container: true,
excludeSemantics: true,
child: Row(
@@ -270,7 +270,9 @@ class _ConversationCardState extends State<ConversationCard> {
}
Widget _caret(SurfaceTokens tokens) {
final label = _collapsed ? 'Expand' : 'Collapse';
final label = _collapsed
? ClideSettings.i18n.string(context, 'card.expand', namespace: 'builtin.claude', placeholder: 'Expand')
: ClideSettings.i18n.string(context, 'card.collapse', namespace: 'builtin.claude', placeholder: 'Collapse');
return Semantics(
button: true,
label: label,
@@ -298,11 +300,11 @@ class _ConversationCardState extends State<ConversationCard> {
case ConversationCardStatus.success:
icon = const CheckIcon();
color = tokens.statusSuccess;
label = 'succeeded';
label = ClideSettings.i18n.string(context, 'card.succeeded', namespace: 'builtin.claude', placeholder: 'succeeded');
case ConversationCardStatus.error:
icon = const CloseIcon();
color = tokens.statusError;
label = 'failed';
label = ClideSettings.i18n.string(context, 'card.failed', namespace: 'builtin.claude', placeholder: 'failed');
case ConversationCardStatus.none:
return const SizedBox.shrink();
}
@@ -331,7 +333,9 @@ class _ConversationCardState extends State<ConversationCard> {
List<Widget> _actions(SurfaceTokens tokens) {
final items = <_ActionItem>[];
if (widget.copyText != null) items.add(_ActionItem('copy', _copy));
if (widget.copyText != null) {
items.add(_ActionItem(ClideSettings.i18n.string(context, 'card.copy', namespace: 'builtin.claude', placeholder: 'copy'), _copy));
}
for (final a in widget.actions) {
items.add(_ActionItem(a.label, a.onInvoke));
}
+78 -38
View File
@@ -303,7 +303,14 @@ class _ConversationViewState extends State<ConversationView> {
if (items.isEmpty) {
return ColoredBox(
color: tokens.panelBackground,
child: widget.emptyState ?? const Center(child: ClideText('Waiting for Claude…', muted: true)),
child:
widget.emptyState ??
Center(
child: ClideText(
ClideSettings.i18n.string(context, 'conversation.empty', namespace: 'builtin.claude', placeholder: 'Waiting for Claude…'),
muted: true,
),
),
);
}
@@ -530,12 +537,20 @@ class _BashLiveTailState extends State<_BashLiveTail> {
Widget build(BuildContext context) {
final term = _terminal;
if (term == null) {
return ClideText('no independent source to follow', muted: true, fontSize: clideFontMeta);
return ClideText(
ClideSettings.i18n.string(context, 'conversation.bashTail.empty', namespace: 'builtin.claude', placeholder: 'no independent source to follow'),
muted: true,
fontSize: clideFontMeta,
);
}
return SizedBox(
height: 160,
child: ClipRect(
child: ClidePtyView(terminal: term, label: 'live tail', fontSize: clideFontMeta),
child: ClidePtyView(
terminal: term,
label: ClideSettings.i18n.string(context, 'conversation.bashTail.label', namespace: 'builtin.claude', placeholder: 'live tail'),
fontSize: clideFontMeta,
),
),
);
}
@@ -614,7 +629,9 @@ class _ConversationTurn extends StatelessWidget {
// carded tool calls.
variant: ConversationCardVariant.bordered,
accent: tokens.globalTextMuted,
label: i.isSidechain ? 'agent prompt' : 'context',
label: i.isSidechain
? ClideSettings.i18n.string(context, 'conversation.label.agentPrompt', namespace: 'builtin.claude', placeholder: 'agent prompt')
: ClideSettings.i18n.string(context, 'conversation.label.context', namespace: 'builtin.claude', placeholder: 'context'),
copyText: i.text,
collapsible: true,
collapsedByDefault: true,
@@ -624,7 +641,7 @@ class _ConversationTurn extends StatelessWidget {
),
UserMessage() => ConversationCard(
accent: tokens.globalFocus,
label: 'you',
label: ClideSettings.i18n.string(context, 'conversation.label.you', namespace: 'builtin.claude', placeholder: 'you'),
copyText: i.text,
margin: _childMargin,
// Pasted-image @path tokens render as inline thumbnails that open the
@@ -644,7 +661,7 @@ class _ConversationTurn extends StatelessWidget {
AssistantTextMessage() when i.synthetic => ConversationCard(
variant: ConversationCardVariant.bordered,
accent: tokens.globalTextMuted,
label: 'clide',
label: ClideSettings.i18n.string(context, 'conversation.label.clide', namespace: 'builtin.claude', placeholder: 'clide'),
copyText: i.text,
margin: _childMargin,
body: ClideText(i.text, muted: true, fontSize: clideFontMeta),
@@ -654,7 +671,9 @@ class _ConversationTurn extends StatelessWidget {
// coral claudeAccent is reserved for the real main-thread Claude.
AssistantTextMessage() => ConversationCard(
accent: i.isSidechain ? tokens.globalTextMuted : claudeAccent,
label: i.isSidechain ? 'agent' : 'claude',
label: i.isSidechain
? ClideSettings.i18n.string(context, 'conversation.label.agent', namespace: 'builtin.claude', placeholder: 'agent')
: ClideSettings.i18n.string(context, 'conversation.label.claude', namespace: 'builtin.claude', placeholder: 'claude'),
copyText: i.text,
margin: _childMargin,
body: ClideMarkdown(
@@ -669,7 +688,9 @@ class _ConversationTurn extends StatelessWidget {
// Framed + muted like the context card (T-306).
variant: ConversationCardVariant.bordered,
accent: tokens.globalTextMuted,
label: i.isSidechain ? 'agent thinking' : 'thinking',
label: i.isSidechain
? ClideSettings.i18n.string(context, 'conversation.label.agentThinking', namespace: 'builtin.claude', placeholder: 'agent thinking')
: ClideSettings.i18n.string(context, 'conversation.label.thinking', namespace: 'builtin.claude', placeholder: 'thinking'),
copyText: i.thinking,
collapsible: true,
collapsedByDefault: true,
@@ -677,8 +698,8 @@ class _ConversationTurn extends StatelessWidget {
margin: _childMargin,
body: ClideText(i.thinking, muted: true, fontSize: clideFontMeta),
),
AssistantToolUse() => collapseTools ? _toolUseCollapser(i) : _toolContentCard(i),
ToolResultMessage() => _toolResult(i),
AssistantToolUse() => collapseTools ? _toolUseCollapser(context, i) : _toolContentCard(context, i),
ToolResultMessage() => _toolResult(context, i),
ImageMessage() => _image(context, i),
};
}
@@ -692,7 +713,7 @@ class _ConversationTurn extends StatelessWidget {
final caption = m.caption;
return ConversationCard(
accent: tokens.globalTextMuted,
label: 'image',
label: ClideSettings.i18n.string(context, 'conversation.label.image', namespace: 'builtin.claude', placeholder: 'image'),
copyText: m.path,
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -712,7 +733,7 @@ class _ConversationTurn extends StatelessWidget {
image: ClideFileImage(m.path),
fit: BoxFit.contain,
alignment: Alignment.centerLeft,
errorBuilder: (_, _, _) => _imagePlaceholder(m.path),
errorBuilder: (_, _, _) => _imagePlaceholder(context, m.path),
),
),
),
@@ -728,12 +749,12 @@ class _ConversationTurn extends StatelessWidget {
ClideKernel.of(context).dialog.show<Object>(
(ctx, dismiss) => ClideLightbox(
onDismiss: dismiss,
child: Image(image: ClideFileImage(path), fit: BoxFit.contain, errorBuilder: (_, _, _) => _imagePlaceholder(path)),
child: Image(image: ClideFileImage(path), fit: BoxFit.contain, errorBuilder: (_, _, _) => _imagePlaceholder(ctx, path)),
),
);
}
Widget _imagePlaceholder(String path) => Container(
Widget _imagePlaceholder(BuildContext context, String path) => Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(color: tokens.panelBorder),
@@ -745,7 +766,18 @@ class _ConversationTurn extends StatelessWidget {
ClideIcon(PhosphorIcons.byName('image'), size: 16, color: tokens.globalTextMuted),
const SizedBox(width: 8),
Flexible(
child: ClideText('could not load $path', fontSize: clideFontMeta, color: tokens.globalTextMuted, maxLines: 1),
child: ClideText(
ClideSettings.i18n.interpolated(
context,
'conversation.imagePlaceholder',
namespace: 'builtin.claude',
placeholder: 'could not load $path',
replacers: [I18nReplacer(from: '{path}', replace: path)],
),
fontSize: clideFontMeta,
color: tokens.globalTextMuted,
maxLines: 1,
),
),
],
),
@@ -757,13 +789,13 @@ class _ConversationTurn extends StatelessWidget {
/// — pushed up from the item; the inner card holds the call body + segments
/// and its own per-item mark. An Agent/Task call also nests its visible
/// sub-agent run in a second collapser below (T-264).
Widget _toolUseCollapser(AssistantToolUse t) {
Widget _toolUseCollapser(BuildContext context, AssistantToolUse t) {
// A Workflow tool-use with a live run (T-416) renders the dedicated run
// card — phases, agent rows, status — instead of the generic tool card. No
// run yet (pre-progress, or on reload where the system events are gone)
// falls through to the generic collapser below.
if (t.name == 'Workflow' && workflows[t.toolUseId] != null) {
return _workflowCard(t, workflows[t.toolUseId]!);
return _workflowCard(context, t, workflows[t.toolUseId]!);
}
final outcome = toolUseOutcomes[t.toolUseId];
final color = outcome == null ? tokens.globalFocus : (outcome ? tokens.statusSuccess : tokens.statusError);
@@ -773,7 +805,7 @@ class _ConversationTurn extends StatelessWidget {
collapsedSummary: _toolUseSummary(t),
counter: '1 step',
status: _toolRunStatus(t),
children: [_toolContentCard(t)],
children: [_toolContentCard(context, t)],
);
final runItems = _isAgentTool(t.name) ? (runByToolUseId[t.toolUseId] ?? const <ConversationItem>[]) : const <ConversationItem>[];
@@ -785,7 +817,7 @@ class _ConversationTurn extends StatelessWidget {
Padding(
padding: const EdgeInsets.only(left: 12),
child: ClideCollapserCard(
label: 'agent run',
label: ClideSettings.i18n.string(context, 'conversation.label.agentRun', namespace: 'builtin.claude', placeholder: 'agent run'),
collapsedSummary: _summarizeActivity(runItems.last),
counter: runItems.length == 1 ? '1 step' : '${runItems.length} steps',
children: [
@@ -814,23 +846,23 @@ class _ConversationTurn extends StatelessWidget {
/// while running, check when done) and a `done/total agents` counter; the body
/// lists each fanned-out agent — grouped under phase headers when the workflow
/// declared phases — plus the run's usage and the orchestration script.
Widget _workflowCard(AssistantToolUse t, WorkflowRun run) {
final title = run.name ?? 'workflow';
Widget _workflowCard(BuildContext context, AssistantToolUse t, WorkflowRun run) {
final title = run.name ?? ClideSettings.i18n.string(context, 'conversation.label.workflow', namespace: 'builtin.claude', placeholder: 'workflow');
final color = run.done ? tokens.statusSuccess : tokens.globalFocus;
final counter = run.agentCount == 0 ? 'starting' : '${run.doneCount}/${run.agentCount} agents';
final detail = run.done ? (run.summary ?? run.description) : run.description;
final collapsedSummary = (detail == null || detail == title) ? title : '$title · $detail';
return ClideCollapserCard(
label: 'workflow',
label: ClideSettings.i18n.string(context, 'conversation.label.workflow', namespace: 'builtin.claude', placeholder: 'workflow'),
color: color,
collapsedSummary: collapsedSummary,
counter: counter,
status: run.done ? ClideRunStatus.success : ClideRunStatus.running,
children: [_workflowBody(t, run)],
children: [_workflowBody(context, t, run)],
);
}
Widget _workflowBody(AssistantToolUse t, WorkflowRun run) {
Widget _workflowBody(BuildContext context, AssistantToolUse t, WorkflowRun run) {
final agents = run.orderedAgents;
final phases = run.orderedPhases;
final rows = <Widget>[];
@@ -851,25 +883,31 @@ class _ConversationTurn extends StatelessWidget {
rows.addAll(agents.where((a) => a.phaseIndex == null).map(_workflowAgentRow));
}
if (rows.isEmpty) {
rows.add(ClideText('Launching…', muted: true, fontSize: clideFontMeta));
rows.add(
ClideText(
ClideSettings.i18n.string(context, 'conversation.workflow.launching', namespace: 'builtin.claude', placeholder: 'Launching…'),
muted: true,
fontSize: clideFontMeta,
),
);
}
final script = t.input['script'];
return ConversationCard(
variant: ConversationCardVariant.bordered,
accent: run.done ? tokens.statusSuccess : tokens.globalFocus,
label: run.name ?? 'workflow',
label: run.name ?? ClideSettings.i18n.string(context, 'conversation.label.workflow', namespace: 'builtin.claude', placeholder: 'workflow'),
copyText: script is String ? script : const JsonEncoder.withIndent(' ').convert(t.input),
body: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: rows),
extraSegments: [
if (run.totalTokens != null && run.totalTokens! > 0)
CardSegment(
label: 'usage',
label: ClideSettings.i18n.string(context, 'conversation.segment.usage', namespace: 'builtin.claude', placeholder: 'usage'),
child: ClideText('${run.totalTokens} tokens${run.durationMs != null ? ' · ${run.durationMs} ms' : ''}', muted: true, fontSize: clideFontMeta),
),
if (script is String)
CardSegment(
label: 'script',
label: ClideSettings.i18n.string(context, 'conversation.segment.script', namespace: 'builtin.claude', placeholder: 'script'),
child: ClideCodeBlock(source: script, language: 'javascript'),
),
],
@@ -906,7 +944,7 @@ class _ConversationTurn extends StatelessWidget {
/// CALL/PROMPT/RESULT segments + its own per-item status mark, with NO own
/// collapse caret — the enclosing collapser owns collapse. Used both as a
/// run/edit child and as the single child of a standalone tool's collapser.
Widget _toolContentCard(AssistantToolUse t) {
Widget _toolContentCard(BuildContext context, AssistantToolUse t) {
// T-262: fold the paired result into this card. A successful result becomes
// a "result" segment below the call + a green header check; a failed result
// stamps a red header cross. No result yet (in-flight) → no mark, no segment.
@@ -926,12 +964,12 @@ class _ConversationTurn extends StatelessWidget {
final segments = <CardSegment>[
for (final p in promptsByToolUseId[t.toolUseId] ?? const <UserMessage>[])
CardSegment(
label: 'prompt',
label: ClideSettings.i18n.string(context, 'conversation.segment.prompt', namespace: 'builtin.claude', placeholder: 'prompt'),
child: ClideText(p.text, muted: true, fontSize: clideFontMeta),
),
if (succeeded && !(isAgent && hasRun))
CardSegment(
label: 'result',
label: ClideSettings.i18n.string(context, 'conversation.segment.result', namespace: 'builtin.claude', placeholder: 'result'),
child: ClideCodeBlock(source: result.content, language: _resultLanguage(t)),
),
// T-325: a Bash card that follows a file (`tail -f …`) gets a live,
@@ -939,7 +977,7 @@ class _ConversationTurn extends StatelessWidget {
// while the card is expanded (the collapser builds segments on expand).
if (t.name == 'Bash' && t.input['command'] is String && bashHasTailIntent(t.input['command'] as String))
CardSegment(
label: 'live tail',
label: ClideSettings.i18n.string(context, 'conversation.segment.liveTail', namespace: 'builtin.claude', placeholder: 'live tail'),
child: _BashLiveTail(command: t.input['command'] as String),
),
];
@@ -955,7 +993,7 @@ class _ConversationTurn extends StatelessWidget {
label: t.name,
copyText: const JsonEncoder.withIndent(' ').convert(t.input),
status: status,
body: toolInputBody(tokens, t.name, t.input, mono),
body: toolInputBody(context, tokens, t.name, t.input, mono),
extraSegments: segments,
// Inside a collapser the surrounding padding is even on all sides
// (T-305): a matching bottom margin is the canvas's bottom inset and the
@@ -987,10 +1025,12 @@ class _ConversationTurn extends StatelessWidget {
}
}
Widget _toolResult(ToolResultMessage t) {
Widget _toolResult(BuildContext context, ToolResultMessage t) {
final paired = toolUseById[t.toolUseId];
final accent = t.isError ? tokens.statusError : tokens.globalTextMuted;
final label = t.isError ? 'error' : 'result';
final label = t.isError
? ClideSettings.i18n.string(context, 'conversation.label.error', namespace: 'builtin.claude', placeholder: 'error')
: ClideSettings.i18n.string(context, 'conversation.label.result', namespace: 'builtin.claude', placeholder: 'result');
// Error result: render the error message prominently (T-168). If we have
// the paired tool_use, show the tool name as a sub-label so the user can
@@ -1002,7 +1042,7 @@ class _ConversationTurn extends StatelessWidget {
if (t.isError) {
final quiet = quietErrorToolUseIds.contains(t.toolUseId);
final multiline = t.content.contains('\n');
final errLabel = quiet ? 'denied' : label;
final errLabel = quiet ? ClideSettings.i18n.string(context, 'conversation.label.denied', namespace: 'builtin.claude', placeholder: 'denied') : label;
return ConversationCard(
variant: ConversationCardVariant.bordered,
accent: quiet ? tokens.globalTextMuted : accent,
@@ -1086,7 +1126,7 @@ class _ActivityCard extends StatelessWidget {
Widget build(BuildContext context) {
final count = items.length;
return ClideCollapserCard(
label: 'Activity',
label: ClideSettings.i18n.string(context, 'conversation.cluster.activity', namespace: 'builtin.claude', placeholder: 'Activity'),
collapsedSummary: _summarizeActivity(items.last),
counter: count == 1 ? '1 step' : '$count steps',
status: _runStatus(items, resultByToolUseId),
@@ -1152,7 +1192,7 @@ class _EditRunCard extends StatelessWidget {
Widget build(BuildContext context) {
final count = edits.length;
return ClideCollapserCard(
label: 'Edits',
label: ClideSettings.i18n.string(context, 'conversation.cluster.edits', namespace: 'builtin.claude', placeholder: 'Edits'),
collapsedSummary: _summarizeActivity(edits.last),
counter: count == 1 ? '1 edit' : '$count edits',
status: _runStatus(edits, resultByToolUseId),
+7 -1
View File
@@ -49,7 +49,13 @@ class ImageThumbnail extends StatelessWidget {
final t = ClideSettings.theme.of(context).surface;
return Semantics(
button: true,
label: 'Image $_fileName',
label: ClideSettings.i18n.interpolated(
context,
'image.semantics',
namespace: 'builtin.claude',
placeholder: 'Image $_fileName',
replacers: [I18nReplacer(from: '{name}', replace: _fileName)],
),
excludeSemantics: true,
child: ClideTappable(
onTap: () => openImageLightbox(context, path),
@@ -49,21 +49,37 @@ class ActivityTabView extends StatelessWidget {
final latest = stats.latest;
final u = usage;
final sections = <MetaSection>[
..._workflowSection(tokens),
..._workflowSection(context, tokens),
if (u != null)
MetaSection('USAGE', [
if (u.session != null) MetaRow('session', u.session!),
if (u.week != null) MetaRow('week (all)', u.week!),
if (u.weekSonnet != null) MetaRow('week (sonnet)', u.weekSonnet!),
MetaSection(ClideSettings.i18n.string(context, 'activity.section.usage', namespace: 'builtin.claude', placeholder: 'USAGE'), [
if (u.session != null)
MetaRow(ClideSettings.i18n.string(context, 'activity.row.session', namespace: 'builtin.claude', placeholder: 'session'), u.session!),
if (u.week != null)
MetaRow(ClideSettings.i18n.string(context, 'activity.row.weekAll', namespace: 'builtin.claude', placeholder: 'week (all)'), u.week!),
if (u.weekSonnet != null)
MetaRow(ClideSettings.i18n.string(context, 'activity.row.weekSonnet', namespace: 'builtin.claude', placeholder: 'week (sonnet)'), u.weekSonnet!),
]),
if (latest != null)
MetaSection('TODAY', [
MetaRow('messages', '${latest.messageCount}'),
MetaRow('sessions', '${latest.sessionCount}'),
MetaRow('tool calls', '${latest.toolCallCount}'),
MetaSection(ClideSettings.i18n.string(context, 'activity.section.today', namespace: 'builtin.claude', placeholder: 'TODAY'), [
MetaRow(ClideSettings.i18n.string(context, 'activity.row.messages', namespace: 'builtin.claude', placeholder: 'messages'), '${latest.messageCount}'),
MetaRow(ClideSettings.i18n.string(context, 'activity.row.sessions', namespace: 'builtin.claude', placeholder: 'sessions'), '${latest.sessionCount}'),
MetaRow(
ClideSettings.i18n.string(context, 'activity.row.toolCalls', namespace: 'builtin.claude', placeholder: 'tool calls'),
'${latest.toolCallCount}',
),
]),
if (latest != null) MetaSection('LIFETIME', [MetaRow('messages', '${stats.lifetimeMessages}'), MetaRow('sessions', '${stats.lifetimeSessions}')]),
..._runtimeSection(tokens),
if (latest != null)
MetaSection(ClideSettings.i18n.string(context, 'activity.section.lifetime', namespace: 'builtin.claude', placeholder: 'LIFETIME'), [
MetaRow(
ClideSettings.i18n.string(context, 'activity.row.messages', namespace: 'builtin.claude', placeholder: 'messages'),
'${stats.lifetimeMessages}',
),
MetaRow(
ClideSettings.i18n.string(context, 'activity.row.sessions', namespace: 'builtin.claude', placeholder: 'sessions'),
'${stats.lifetimeSessions}',
),
]),
..._runtimeSection(context, tokens),
];
return ListView(
@@ -73,20 +89,57 @@ class ActivityTabView extends StatelessWidget {
// the builtin.claude/command bus — identical to typing the command.
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: ClideText('SESSION', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader),
child: ClideText(
ClideSettings.i18n.string(context, 'activity.section.session', namespace: 'builtin.claude', placeholder: 'SESSION'),
fontSize: clideFontSmall,
color: tokens.sidebarSectionHeader,
),
),
Row(
children: [
_control(context, tokens, 'clear', 'trash', '/clear'),
_control(context, tokens, 'compact', 'arrows-in-simple', '/compact'),
_control(context, tokens, 'fork', 'git-branch', '/fork'),
_control(context, tokens, 'resume', 'clock-counter-clockwise', '/resume'),
_control(
context,
tokens,
ClideSettings.i18n.string(context, 'activity.control.clear', namespace: 'builtin.claude', placeholder: 'clear'),
'trash',
'/clear',
),
_control(
context,
tokens,
ClideSettings.i18n.string(context, 'activity.control.compact', namespace: 'builtin.claude', placeholder: 'compact'),
'arrows-in-simple',
'/compact',
),
_control(
context,
tokens,
ClideSettings.i18n.string(context, 'activity.control.fork', namespace: 'builtin.claude', placeholder: 'fork'),
'git-branch',
'/fork',
),
_control(
context,
tokens,
ClideSettings.i18n.string(context, 'activity.control.resume', namespace: 'builtin.claude', placeholder: 'resume'),
'clock-counter-clockwise',
'/resume',
),
const Spacer(),
_control(context, tokens, 'refresh usage', 'arrow-clockwise', '/usage'),
_control(
context,
tokens,
ClideSettings.i18n.string(context, 'activity.control.refreshUsage', namespace: 'builtin.claude', placeholder: 'refresh usage'),
'arrow-clockwise',
'/usage',
),
],
),
const SizedBox(height: 6),
if (sections.isEmpty) metaPlaceholder('No activity recorded yet.') else ...metaTableChildren(tokens, sections),
if (sections.isEmpty)
metaPlaceholder(ClideSettings.i18n.string(context, 'activity.empty', namespace: 'builtin.claude', placeholder: 'No activity recorded yet.'))
else
...metaTableChildren(tokens, sections),
],
);
}
@@ -94,11 +147,26 @@ class ActivityTabView extends StatelessWidget {
Widget _control(BuildContext context, SurfaceTokens tokens, String label, String glyph, String command) {
return Semantics(
button: true,
label: '$label session',
label: ClideSettings.i18n.interpolated(
context,
'activity.control.semantics',
namespace: 'builtin.claude',
placeholder: '$label session',
replacers: [I18nReplacer(from: '{label}', replace: label)],
),
excludeSemantics: true,
onTap: () => _command(context, command),
child: ClideTappable(
tooltip: '$label · $command',
tooltip: ClideSettings.i18n.interpolated(
context,
'activity.control.tooltip',
namespace: 'builtin.claude',
placeholder: '$label · $command',
replacers: [
I18nReplacer(from: '{label}', replace: label),
I18nReplacer(from: '{command}', replace: command),
],
),
onTap: () => _command(context, command),
builder: (ctx, hovered, _) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
@@ -111,31 +179,50 @@ class ActivityTabView extends StatelessWidget {
/// An aggregate WORKFLOWS section while one or more workflow runs exist this
/// session (T-416): a row per run — its name and `done/total agents`, tinted
/// focus while running and success once complete.
List<MetaSection> _workflowSection(SurfaceTokens tokens) {
List<MetaSection> _workflowSection(BuildContext context, SurfaceTokens tokens) {
final runs = workflows.values.toList();
if (runs.isEmpty) return const [];
final done = ClideSettings.i18n.string(context, 'activity.workflow.done', namespace: 'builtin.claude', placeholder: 'done');
final starting = ClideSettings.i18n.string(context, 'activity.workflow.starting', namespace: 'builtin.claude', placeholder: 'starting');
final fallback = ClideSettings.i18n.string(context, 'activity.workflow.fallback', namespace: 'builtin.claude', placeholder: 'workflow');
return [
MetaSection('WORKFLOWS', [
MetaSection(ClideSettings.i18n.string(context, 'activity.section.workflows', namespace: 'builtin.claude', placeholder: 'WORKFLOWS'), [
for (final r in runs)
MetaRow(
r.name ?? r.taskId ?? 'workflow',
r.agentCount == 0 ? (r.done ? 'done' : 'starting') : '${r.doneCount}/${r.agentCount} agents${r.done ? '' : ''}',
r.name ?? r.taskId ?? fallback,
r.agentCount == 0 ? (r.done ? done : starting) : '${r.doneCount}/${r.agentCount} agents${r.done ? '' : ''}',
valueColor: r.done ? tokens.statusSuccess : tokens.globalFocus,
),
]),
];
}
List<MetaSection> _runtimeSection(SurfaceTokens tokens) {
List<MetaSection> _runtimeSection(BuildContext context, SurfaceTokens tokens) {
final st = primaryStatus;
final skills = config?.skills.length;
final rows = <MetaRow>[
if (st?.model != null) MetaRow('model', shortModelLabel(st!.model!), valueColor: tokens.globalFocus),
if (st?.effort != null) MetaRow('effort', st!.effort!),
if (st?.contextTokens != null) MetaRow('context', '${formatTokenCount(st!.contextTokens!)} ctx'),
if (st?.permissionMode != null) MetaRow('mode', permissionModeLabel(st!.permissionMode!)),
if (skills != null) MetaRow('skills', '$skills'),
if (st?.model != null)
MetaRow(
ClideSettings.i18n.string(context, 'activity.row.model', namespace: 'builtin.claude', placeholder: 'model'),
shortModelLabel(st!.model!),
valueColor: tokens.globalFocus,
),
if (st?.effort != null)
MetaRow(ClideSettings.i18n.string(context, 'activity.row.effort', namespace: 'builtin.claude', placeholder: 'effort'), st!.effort!),
if (st?.contextTokens != null)
MetaRow(
ClideSettings.i18n.string(context, 'activity.row.context', namespace: 'builtin.claude', placeholder: 'context'),
'${formatTokenCount(st!.contextTokens!)} ctx',
),
if (st?.permissionMode != null)
MetaRow(
ClideSettings.i18n.string(context, 'activity.row.mode', namespace: 'builtin.claude', placeholder: 'mode'),
permissionModeLabel(st!.permissionMode!),
),
if (skills != null) MetaRow(ClideSettings.i18n.string(context, 'activity.row.skills', namespace: 'builtin.claude', placeholder: 'skills'), '$skills'),
];
return rows.isEmpty ? const [] : [MetaSection('RUNTIME · primary', rows)];
return rows.isEmpty
? const []
: [MetaSection(ClideSettings.i18n.string(context, 'activity.section.runtime', namespace: 'builtin.claude', placeholder: 'RUNTIME · primary'), rows)];
}
}
@@ -44,7 +44,7 @@ class ConfigTabView extends StatelessWidget {
final tokens = ClideSettings.theme.of(context).surface;
final cfg = config;
if (cfg == null) {
return metaPlaceholder('Claude environment not loaded.');
return metaPlaceholder(ClideSettings.i18n.string(context, 'config.empty', namespace: 'builtin.claude', placeholder: 'Claude environment not loaded.'));
}
final settings = cfg.settings;
final model = status?.model ?? cfg.probe?.model ?? settings['model']?.toString() ?? 'default';
@@ -56,26 +56,40 @@ class ConfigTabView extends StatelessWidget {
// Pinned SETTINGS control panel — not collapsible.
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: ClideText('SETTINGS', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader),
child: ClideText(
ClideSettings.i18n.string(context, 'config.section.settings', namespace: 'builtin.claude', placeholder: 'SETTINGS'),
fontSize: clideFontSmall,
color: tokens.sidebarSectionHeader,
),
),
SettingControlRow(
label: 'model',
label: ClideSettings.i18n.string(context, 'config.row.model', namespace: 'builtin.claude', placeholder: 'model'),
value: model,
valueColor: tokens.globalFocus,
options: (models == null || models!.isEmpty) ? kFallbackModels : models!,
isActive: (o) => o.value == model || model.toLowerCase().contains(o.value.toLowerCase()),
command: 'model',
),
SettingControlRow(label: 'effort', value: effort, options: kEffortLevels, isActive: (o) => o.value == effort, command: 'effort'),
SettingControlRow(
label: 'permission mode',
label: ClideSettings.i18n.string(context, 'config.row.effort', namespace: 'builtin.claude', placeholder: 'effort'),
value: effort,
options: kEffortLevels,
isActive: (o) => o.value == effort,
command: 'effort',
),
SettingControlRow(
label: ClideSettings.i18n.string(context, 'config.row.permissionMode', namespace: 'builtin.claude', placeholder: 'permission mode'),
value: permissionModeLabel(mode),
options: kPermissionModes,
isActive: (o) => o.value == mode,
command: 'permissions',
),
_configRow(tokens, 'output style', outputStyle),
_configRow(tokens, 'source', '~/.claude + .claude'),
_configRow(tokens, ClideSettings.i18n.string(context, 'config.row.outputStyle', namespace: 'builtin.claude', placeholder: 'output style'), outputStyle),
_configRow(
tokens,
ClideSettings.i18n.string(context, 'config.row.source', namespace: 'builtin.claude', placeholder: 'source'),
ClideSettings.i18n.string(context, 'config.row.source.value', namespace: 'builtin.claude', placeholder: '~/.claude + .claude'),
),
// ---- Accordion sections ----
for (final section in ConfigSection.values) _accordion(context, tokens, cfg, section),
@@ -83,7 +97,16 @@ class ConfigTabView extends StatelessWidget {
// Footer hint.
Padding(
padding: const EdgeInsets.only(top: 12),
child: ClideText('expand a list to see all · click a skill/agent/command → opens its .md', muted: true, fontSize: clideFontSmall),
child: ClideText(
ClideSettings.i18n.string(
context,
'config.footer',
namespace: 'builtin.claude',
placeholder: 'expand a list to see all · click a skill/agent/command → opens its .md',
),
muted: true,
fontSize: clideFontSmall,
),
),
];
@@ -109,13 +132,13 @@ class ConfigTabView extends StatelessWidget {
);
}
String _sectionLabel(ConfigSection section) => switch (section) {
ConfigSection.skills => 'SKILLS',
ConfigSection.agents => 'AGENTS',
ConfigSection.commands => 'COMMANDS',
ConfigSection.hooks => 'HOOKS',
ConfigSection.permissions => 'PERMISSIONS',
ConfigSection.mcpServers => 'MCP SERVERS',
String _sectionLabel(BuildContext context, ConfigSection section) => switch (section) {
ConfigSection.skills => ClideSettings.i18n.string(context, 'config.section.skills', namespace: 'builtin.claude', placeholder: 'SKILLS'),
ConfigSection.agents => ClideSettings.i18n.string(context, 'config.section.agents', namespace: 'builtin.claude', placeholder: 'AGENTS'),
ConfigSection.commands => ClideSettings.i18n.string(context, 'config.section.commands', namespace: 'builtin.claude', placeholder: 'COMMANDS'),
ConfigSection.hooks => ClideSettings.i18n.string(context, 'config.section.hooks', namespace: 'builtin.claude', placeholder: 'HOOKS'),
ConfigSection.permissions => ClideSettings.i18n.string(context, 'config.section.permissions', namespace: 'builtin.claude', placeholder: 'PERMISSIONS'),
ConfigSection.mcpServers => ClideSettings.i18n.string(context, 'config.section.mcpServers', namespace: 'builtin.claude', placeholder: 'MCP SERVERS'),
};
int _sectionCount(ClaudeConfig config, ConfigSection section) => switch (section) {
@@ -131,7 +154,7 @@ class ConfigTabView extends StatelessWidget {
final isExpanded = expanded.contains(section);
final children = isExpanded ? _sectionChildren(context, tokens, config, section) : const <Widget>[];
return ClideAccordion(
label: _sectionLabel(section),
label: _sectionLabel(context, section),
count: _sectionCount(config, section),
expanded: isExpanded,
onToggle: () => onToggleSection(section),
@@ -166,7 +189,7 @@ class ConfigTabView extends StatelessWidget {
),
];
case ConfigSection.permissions:
return _permissionRows(tokens, config.permissions);
return _permissionRows(context, tokens, config.permissions);
case ConfigSection.mcpServers:
return [
for (final srv in config.mcpServers)
@@ -205,7 +228,7 @@ class ConfigTabView extends StatelessWidget {
}
/// Renders grouped allow/ask/deny permission rows, colour-coded by kind.
List<Widget> _permissionRows(SurfaceTokens tokens, ClaudePermissions perms) {
List<Widget> _permissionRows(BuildContext context, SurfaceTokens tokens, ClaudePermissions perms) {
// allow → statusSuccess, ask → statusWarning, deny → statusError
Color kindColor(ConfigPermKind k) => switch (k) {
ConfigPermKind.allow => tokens.statusSuccess,
@@ -214,9 +237,9 @@ class ConfigTabView extends StatelessWidget {
};
String kindLabel(ConfigPermKind k) => switch (k) {
ConfigPermKind.allow => 'allow',
ConfigPermKind.ask => 'ask',
ConfigPermKind.deny => 'deny',
ConfigPermKind.allow => ClideSettings.i18n.string(context, 'config.perm.allow', namespace: 'builtin.claude', placeholder: 'allow'),
ConfigPermKind.ask => ClideSettings.i18n.string(context, 'config.perm.ask', namespace: 'builtin.claude', placeholder: 'ask'),
ConfigPermKind.deny => ClideSettings.i18n.string(context, 'config.perm.deny', namespace: 'builtin.claude', placeholder: 'deny'),
};
final groups = [(ConfigPermKind.allow, perms.allow), (ConfigPermKind.ask, perms.ask), (ConfigPermKind.deny, perms.deny)];
@@ -320,18 +343,42 @@ class _SettingControlRowState extends State<SettingControlRow> {
ClideMenuItem(
label: o.description.isEmpty ? o.displayName : '${o.displayName}${o.description}',
active: widget.isActive(o),
semanticLabel: '${widget.label}: ${o.displayName}',
semanticLabel: ClideSettings.i18n.interpolated(
context,
'config.control.option.semantics',
namespace: 'builtin.claude',
placeholder: '${widget.label}: ${o.displayName}',
replacers: [
I18nReplacer(from: '{label}', replace: widget.label),
I18nReplacer(from: '{name}', replace: o.displayName),
],
),
onSelect: () => _pick(o.value),
),
],
),
anchor: Semantics(
button: true,
label: '${widget.label}: ${widget.value}. Click to change.',
label: ClideSettings.i18n.interpolated(
context,
'config.control.semantics',
namespace: 'builtin.claude',
placeholder: '${widget.label}: ${widget.value}. Click to change.',
replacers: [
I18nReplacer(from: '{label}', replace: widget.label),
I18nReplacer(from: '{value}', replace: widget.value),
],
),
excludeSemantics: true,
onTap: _overlay.toggle,
child: ClideTappable(
tooltip: 'change ${widget.label}',
tooltip: ClideSettings.i18n.interpolated(
context,
'config.control.tooltip',
namespace: 'builtin.claude',
placeholder: 'change ${widget.label}',
replacers: [I18nReplacer(from: '{label}', replace: widget.label)],
),
onTap: _overlay.toggle,
builder: (ctx, hovered, _) => DecoratedBox(
decoration: BoxDecoration(color: hovered ? tokens.listItemHoverBackground : null, borderRadius: BorderRadius.circular(4)),
@@ -40,15 +40,27 @@ class PermissionModeBadge extends StatelessWidget {
final isBypass = mode == 'bypassPermissions';
final badgeColor = isBypass ? const Color(0xFFF06C6F) : tokens.globalFocus;
final tooltip =
'Permission mode: ${permissionModeLabel(mode)}. '
'Click to cycle default/acceptEdits/plan; Shift-click for bypassPermissions.';
final tooltip = ClideSettings.i18n.interpolated(
context,
'permissionBadge.tooltip',
namespace: 'builtin.claude',
placeholder:
'Permission mode: ${permissionModeLabel(mode)}. '
'Click to cycle default/acceptEdits/plan; Shift-click for bypassPermissions.',
replacers: [I18nReplacer(from: '{mode}', replace: permissionModeLabel(mode))],
);
return Padding(
padding: const EdgeInsets.only(top: 3),
child: Semantics(
button: true,
label: 'Permission mode: $label',
label: ClideSettings.i18n.interpolated(
context,
'permissionBadge.semantics',
namespace: 'builtin.claude',
placeholder: 'Permission mode: $label',
replacers: [I18nReplacer(from: '{label}', replace: label)],
),
excludeSemantics: true,
onTap: () {
if (HardwareKeyboard.instance.isShiftPressed) {
@@ -154,27 +154,40 @@ class _AgentRosterRowState extends State<AgentRosterRow> {
child: Row(
children: [
Expanded(
child: ClideText('Enable bypassPermissions? All tool calls will be auto-allowed.', fontSize: clideFontSmall, color: tokens.globalTextMuted),
child: ClideText(
ClideSettings.i18n.string(
context,
'roster.bypass.confirmBody',
namespace: 'builtin.claude',
placeholder: 'Enable bypassPermissions? All tool calls will be auto-allowed.',
),
fontSize: clideFontSmall,
color: tokens.globalTextMuted,
),
),
const SizedBox(width: 4),
// Confirm
Semantics(
button: true,
label: 'Confirm bypass',
label: ClideSettings.i18n.string(context, 'roster.bypass.confirm.semantics', namespace: 'builtin.claude', placeholder: 'Confirm bypass'),
excludeSemantics: true,
onTap: () {
setState(() => _confirmingBypass = false);
widget.onSetPermissionMode(widget.member.name, 'bypassPermissions');
},
child: ClideTappable(
tooltip: 'Confirm',
tooltip: ClideSettings.i18n.string(context, 'roster.bypass.confirm.tooltip', namespace: 'builtin.claude', placeholder: 'Confirm'),
onTap: () {
setState(() => _confirmingBypass = false);
widget.onSetPermissionMode(widget.member.name, 'bypassPermissions');
},
builder: (ctx, hovered, _) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 2),
child: ClideText('OK', fontSize: clideFontSmall, color: hovered ? tokens.globalForeground : tokens.globalFocus),
child: ClideText(
ClideSettings.i18n.string(context, 'roster.bypass.ok', namespace: 'builtin.claude', placeholder: 'OK'),
fontSize: clideFontSmall,
color: hovered ? tokens.globalForeground : tokens.globalFocus,
),
),
),
),
@@ -182,15 +195,19 @@ class _AgentRosterRowState extends State<AgentRosterRow> {
// Cancel
Semantics(
button: true,
label: 'Cancel bypass',
label: ClideSettings.i18n.string(context, 'roster.bypass.cancel.semantics', namespace: 'builtin.claude', placeholder: 'Cancel bypass'),
excludeSemantics: true,
onTap: () => setState(() => _confirmingBypass = false),
child: ClideTappable(
tooltip: 'Cancel',
tooltip: ClideSettings.i18n.string(context, 'roster.bypass.cancel.tooltip', namespace: 'builtin.claude', placeholder: 'Cancel'),
onTap: () => setState(() => _confirmingBypass = false),
builder: (ctx, hovered, _) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 2),
child: ClideText('Cancel', fontSize: clideFontSmall, color: hovered ? tokens.globalForeground : tokens.globalTextMuted),
child: ClideText(
ClideSettings.i18n.string(context, 'roster.bypass.cancel', namespace: 'builtin.claude', placeholder: 'Cancel'),
fontSize: clideFontSmall,
color: hovered ? tokens.globalForeground : tokens.globalTextMuted,
),
),
),
),
@@ -206,7 +223,9 @@ class _AgentRosterRowState extends State<AgentRosterRow> {
// Show / hide
MetaIconButton(
painter: isVisible ? PhosphorIcons.byName('eye') : PhosphorIcons.byName('eye-slash'),
tooltip: isVisible ? 'Hide pane' : 'Show pane',
tooltip: isVisible
? ClideSettings.i18n.string(context, 'roster.hidePane', namespace: 'builtin.claude', placeholder: 'Hide pane')
: ClideSettings.i18n.string(context, 'roster.showPane', namespace: 'builtin.claude', placeholder: 'Show pane'),
color: tokens.globalTextMuted,
onTap: () => isVisible ? widget.orchestrator!.hide(managed.id) : widget.orchestrator!.show(managed.id),
),
@@ -216,28 +235,30 @@ class _AgentRosterRowState extends State<AgentRosterRow> {
// NOTE: We use eye/eyeSlash as stand-ins until a dedicated speaker
// icon is added to PhosphorIcons (no speaker codepoint yet).
// The semantic tooltip still says mute/unmute so AT users are clear.
tooltip: isMuted ? 'Unmute messages' : 'Mute messages',
tooltip: isMuted
? ClideSettings.i18n.string(context, 'roster.unmute', namespace: 'builtin.claude', placeholder: 'Unmute messages')
: ClideSettings.i18n.string(context, 'roster.mute', namespace: 'builtin.claude', placeholder: 'Mute messages'),
color: isMuted ? tokens.globalFocus : tokens.globalTextMuted,
onTap: () => isMuted ? widget.orchestrator!.unmute(managed.id) : widget.orchestrator!.mute(managed.id),
),
// Inject message
MetaIconButton(
painter: PhosphorIcons.byName('chat-circle'),
tooltip: 'Inject message',
tooltip: ClideSettings.i18n.string(context, 'roster.inject', namespace: 'builtin.claude', placeholder: 'Inject message'),
color: isInjecting ? tokens.globalFocus : tokens.globalTextMuted,
onTap: () => widget.onToggleInject(widget.member.name),
),
// Fork session (T-172): branch into a new pane without touching the original.
MetaIconButton(
painter: PhosphorIcons.byName('git-branch'),
tooltip: 'Fork session',
tooltip: ClideSettings.i18n.string(context, 'roster.fork', namespace: 'builtin.claude', placeholder: 'Fork session'),
color: tokens.globalTextMuted,
onTap: () => widget.onFork(widget.member.name),
),
// Close session
MetaIconButton(
painter: PhosphorIcons.byName('x'),
tooltip: 'Close session',
tooltip: ClideSettings.i18n.string(context, 'roster.close', namespace: 'builtin.claude', placeholder: 'Close session'),
color: tokens.globalTextMuted,
onTap: () => widget.onClose(widget.member.name),
),
@@ -262,7 +283,7 @@ class _AgentRosterRowState extends State<AgentRosterRow> {
const SizedBox(width: 4),
MetaIconButton(
painter: PhosphorIcons.byName('x'),
tooltip: 'Cancel',
tooltip: ClideSettings.i18n.string(context, 'roster.inject.cancel', namespace: 'builtin.claude', placeholder: 'Cancel'),
color: tokens.globalTextMuted,
onTap: () => widget.onToggleInject(widget.member.name),
),
@@ -29,7 +29,7 @@ class SidebarTabStrip extends StatelessWidget {
child: Semantics(
button: true,
selected: t == current,
label: _label(t),
label: _label(context, t),
excludeSemantics: true,
onTap: () => onPick(t),
child: ClideTappable(
@@ -39,7 +39,11 @@ class SidebarTabStrip extends StatelessWidget {
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: t == current ? tokens.globalFocus : const Color(0x00000000), width: 2)),
),
child: ClideText(_label(t), fontSize: clideFontSmall, color: t == current || hovered ? tokens.globalForeground : tokens.globalTextMuted),
child: ClideText(
_label(context, t),
fontSize: clideFontSmall,
color: t == current || hovered ? tokens.globalForeground : tokens.globalTextMuted,
),
),
),
),
@@ -49,9 +53,18 @@ class SidebarTabStrip extends StatelessWidget {
);
}
String _label(SidebarTab t) => switch (t) {
SidebarTab.activity => 'Activity',
SidebarTab.team => memberCount == 0 ? 'Team' : 'Team · $memberCount',
SidebarTab.config => 'Config',
String _label(BuildContext context, SidebarTab t) => switch (t) {
SidebarTab.activity => ClideSettings.i18n.string(context, 'tabStrip.activity', namespace: 'builtin.claude', placeholder: 'Activity'),
SidebarTab.team =>
memberCount == 0
? ClideSettings.i18n.string(context, 'tabStrip.team', namespace: 'builtin.claude', placeholder: 'Team')
: ClideSettings.i18n.interpolated(
context,
'tabStrip.team.count',
namespace: 'builtin.claude',
placeholder: 'Team · $memberCount',
replacers: [I18nReplacer(from: '{count}', replace: '$memberCount')],
),
SidebarTab.config => ClideSettings.i18n.string(context, 'tabStrip.config', namespace: 'builtin.claude', placeholder: 'Config'),
};
}
@@ -54,7 +54,7 @@ class TaskRow extends StatelessWidget {
if (broker != null && broker!.members.length > 1)
MetaIconButton(
painter: PhosphorIcons.byName('arrow-clockwise'),
tooltip: 'Reassign task',
tooltip: ClideSettings.i18n.string(context, 'taskRow.reassign', namespace: 'builtin.claude', placeholder: 'Reassign task'),
color: tokens.globalTextMuted,
onTap: () => _reassign(context),
),
@@ -50,7 +50,7 @@ class TeamTabView extends StatelessWidget {
Widget build(BuildContext context) {
final tokens = ClideSettings.theme.of(context).surface;
if (members.isEmpty) {
return metaPlaceholder('No team active.');
return metaPlaceholder(ClideSettings.i18n.string(context, 'team.empty', namespace: 'builtin.claude', placeholder: 'No team active.'));
}
final children = <Widget>[
for (final m in members)
@@ -71,7 +71,7 @@ class TeamTabView extends StatelessWidget {
if (tasks.isNotEmpty) {
children.add(const SizedBox(height: 12));
children.add(_taskSection(tokens));
children.add(_taskSection(context, tokens));
}
// MESSAGES section (T-180): live broker chat feed + quick-post composer.
@@ -85,11 +85,15 @@ class TeamTabView extends StatelessWidget {
return ListView(padding: const EdgeInsets.all(12), children: children);
}
Widget _taskSection(SurfaceTokens tokens) {
Widget _taskSection(BuildContext context, SurfaceTokens tokens) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClideText('TASKS', fontSize: clideFontSmall, color: tokens.globalTextMuted),
ClideText(
ClideSettings.i18n.string(context, 'team.section.tasks', namespace: 'builtin.claude', placeholder: 'TASKS'),
fontSize: clideFontSmall,
color: tokens.globalTextMuted,
),
const SizedBox(height: 4),
for (final t in tasks) TaskRow(task: t, members: members, broker: orchestrator?.broker),
],
@@ -162,7 +162,11 @@ class _ModelPickerCardState extends State<ModelPickerCard> {
Row(
children: [
const Spacer(),
ClideButton(label: 'cancel', variant: ClideButtonVariant.subtle, onPressed: widget.onCancel),
ClideButton(
label: ClideSettings.i18n.string(context, 'modelPicker.cancel', namespace: 'builtin.claude', placeholder: 'cancel'),
variant: ClideButtonVariant.subtle,
onPressed: widget.onCancel,
),
],
),
],
@@ -101,11 +101,23 @@ class _PermissionModeControlState extends State<PermissionModeControl> {
final open = _overlay.isOpen;
return Semantics(
button: true,
label: 'permission mode: ${permissionModeLabel(widget.mode)}. Activate to change.',
label: ClideSettings.i18n.interpolated(
context,
'permissionControl.semantics',
namespace: 'builtin.claude',
placeholder: 'permission mode: ${permissionModeLabel(widget.mode)}. Activate to change.',
replacers: [I18nReplacer(from: '{mode}', replace: permissionModeLabel(widget.mode))],
),
excludeSemantics: true,
child: ClideTappable(
onTap: _overlay.toggle,
tooltip: 'Permission mode: ${permissionModeLabel(widget.mode)} — change (Ctrl/Cmd+M cycles)',
tooltip: ClideSettings.i18n.interpolated(
context,
'permissionControl.tooltip',
namespace: 'builtin.claude',
placeholder: 'Permission mode: ${permissionModeLabel(widget.mode)} — change (Ctrl/Cmd+M cycles)',
replacers: [I18nReplacer(from: '{mode}', replace: permissionModeLabel(widget.mode))],
),
builder: (ctx, hovered, _) => Container(
width: 28,
height: 28,
+140 -30
View File
@@ -244,7 +244,13 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
final canRemember = p.permissionSuggestions.isNotEmpty;
return (
tokens.statusWarning,
'permission · ${p.displayName}',
ClideSettings.i18n.interpolated(
context,
'prompt.permission.label',
namespace: 'builtin.claude',
placeholder: 'permission · ${p.displayName}',
replacers: [I18nReplacer(from: '{name}', replace: p.displayName)],
),
[
// Claude's description for Write/Edit is often just the file_path,
// which the body already shows via _pathLine — suppress in that case
@@ -265,25 +271,66 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
spacing: 8,
runSpacing: 8,
children: [
ClideButton(label: '1. Allow', variant: ClideButtonVariant.primary, onPressed: () => _permAllow()),
if (canRemember) ClideButton(label: "2. Allow & don't ask again", onPressed: () => _permAllow(remember: true)),
ClideButton(label: '${canRemember ? '3' : '2'}. Deny', onPressed: _permDeny),
ClideButton(
label: '${canRemember ? '4' : '3'}. Deny & simplify',
tooltip: 'Deny and ask Claude to retry this action in a simpler format — complex interactions don\'t work well with the permission system.',
label: ClideSettings.i18n.string(context, 'prompt.permission.allow', namespace: 'builtin.claude', placeholder: '1. Allow'),
variant: ClideButtonVariant.primary,
onPressed: () => _permAllow(),
),
if (canRemember)
ClideButton(
label: ClideSettings.i18n.string(
context,
'prompt.permission.allowRemember',
namespace: 'builtin.claude',
placeholder: "2. Allow & don't ask again",
),
onPressed: () => _permAllow(remember: true),
),
ClideButton(
label: ClideSettings.i18n.interpolated(
context,
'prompt.permission.deny',
namespace: 'builtin.claude',
placeholder: '${canRemember ? '3' : '2'}. Deny',
replacers: [I18nReplacer(from: '{n}', replace: canRemember ? '3' : '2')],
),
onPressed: _permDeny,
),
ClideButton(
label: ClideSettings.i18n.interpolated(
context,
'prompt.permission.denySimplify',
namespace: 'builtin.claude',
placeholder: '${canRemember ? '4' : '3'}. Deny & simplify',
replacers: [I18nReplacer(from: '{n}', replace: canRemember ? '4' : '3')],
),
tooltip: ClideSettings.i18n.string(
context,
'prompt.permission.denySimplify.tooltip',
namespace: 'builtin.claude',
placeholder: 'Deny and ask Claude to retry this action in a simpler format — complex interactions don\'t work well with the permission system.',
),
onPressed: _permDenySimplify,
),
],
),
const SizedBox(height: 10),
_NoteField(controller: _note, placeholder: 'add a note (optional) — sent to Claude'),
_NoteField(
controller: _note,
placeholder: ClideSettings.i18n.string(
context,
'prompt.permission.note.placeholder',
namespace: 'builtin.claude',
placeholder: 'add a note (optional) — sent to Claude',
),
),
],
);
}
/// Render the tool input in the shape that best fits the tool. Delegates to
/// the shared top-level helpers (also used by ConversationView — T-168).
Widget _inputBody(SurfaceTokens tokens, String mono, ToolPrompt p) => toolInputBody(tokens, p.toolName, p.input, mono);
Widget _inputBody(SurfaceTokens tokens, String mono, ToolPrompt p) => toolInputBody(context, tokens, p.toolName, p.input, mono);
// -- AskUserQuestion: single = bare, multi = stepper + review --------------
@@ -291,13 +338,17 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
if (_q.length <= 1) {
return (
tokens.statusInfo,
'question',
ClideSettings.i18n.string(context, 'prompt.question.label', namespace: 'builtin.claude', placeholder: 'question'),
[
if (_q.isNotEmpty) _questionBody(tokens, 0),
const SizedBox(height: 6),
Row(
children: [
ClideButton(label: 'Submit', variant: ClideButtonVariant.primary, onPressed: (_q.isNotEmpty && _answer(0).isNotEmpty) ? _submit : null),
ClideButton(
label: ClideSettings.i18n.string(context, 'prompt.submit', namespace: 'builtin.claude', placeholder: 'Submit'),
variant: ClideButtonVariant.primary,
onPressed: (_q.isNotEmpty && _answer(0).isNotEmpty) ? _submit : null,
),
const Spacer(),
_chatInstead(tokens),
],
@@ -308,17 +359,28 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
if (_step >= _q.length) {
return (
tokens.statusInfo,
'review',
ClideSettings.i18n.string(context, 'prompt.review.label', namespace: 'builtin.claude', placeholder: 'review'),
[
ClideText('Review your answers', color: tokens.globalForeground, fontSize: clideFontBody),
ClideText(
ClideSettings.i18n.string(context, 'prompt.review.title', namespace: 'builtin.claude', placeholder: 'Review your answers'),
color: tokens.globalForeground,
fontSize: clideFontBody,
),
const SizedBox(height: 8),
for (var i = 0; i < _q.length; i++) _reviewRow(tokens, i),
const SizedBox(height: 6),
Row(
children: [
ClideButton(label: ' Back', onPressed: () => setState(() => _step = _q.length - 1)),
ClideButton(
label: ClideSettings.i18n.string(context, 'prompt.back', namespace: 'builtin.claude', placeholder: ' Back'),
onPressed: () => setState(() => _step = _q.length - 1),
),
const SizedBox(width: 8),
ClideButton(label: 'Submit answers', variant: ClideButtonVariant.primary, onPressed: _allAnswered ? _submit : null),
ClideButton(
label: ClideSettings.i18n.string(context, 'prompt.submitAnswers', namespace: 'builtin.claude', placeholder: 'Submit answers'),
variant: ClideButtonVariant.primary,
onPressed: _allAnswered ? _submit : null,
),
],
),
],
@@ -328,7 +390,7 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
final last = _step == _q.length - 1;
return (
tokens.statusInfo,
'question',
ClideSettings.i18n.string(context, 'prompt.question.label', namespace: 'builtin.claude', placeholder: 'question'),
[
_navBar(tokens),
const SizedBox(height: 10),
@@ -336,8 +398,20 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
const SizedBox(height: 6),
Row(
children: [
if (_step > 0) ...[ClideButton(label: ' Back', onPressed: () => setState(() => _step--)), const SizedBox(width: 8)],
ClideButton(label: last ? 'Review ' : 'Next ', variant: ClideButtonVariant.primary, onPressed: answered ? () => setState(() => _step++) : null),
if (_step > 0) ...[
ClideButton(
label: ClideSettings.i18n.string(context, 'prompt.back', namespace: 'builtin.claude', placeholder: ' Back'),
onPressed: () => setState(() => _step--),
),
const SizedBox(width: 8),
],
ClideButton(
label: last
? ClideSettings.i18n.string(context, 'prompt.reviewNav', namespace: 'builtin.claude', placeholder: 'Review ')
: ClideSettings.i18n.string(context, 'prompt.next', namespace: 'builtin.claude', placeholder: 'Next '),
variant: ClideButtonVariant.primary,
onPressed: answered ? () => setState(() => _step++) : null,
),
const Spacer(),
_chatInstead(tokens),
],
@@ -375,7 +449,14 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
);
}
}
chips.add(ClideText('Review', fontSize: clideFontMeta, fontFamily: ClideSettings.fonts.monoOf(context), color: tokens.globalTextMuted));
chips.add(
ClideText(
ClideSettings.i18n.string(context, 'prompt.nav.review', namespace: 'builtin.claude', placeholder: 'Review'),
fontSize: clideFontMeta,
fontFamily: ClideSettings.fonts.monoOf(context),
color: tokens.globalTextMuted,
),
);
chips.add(ClideText('', color: tokens.globalTextMuted, fontSize: 15));
return Wrap(spacing: 10, runSpacing: 6, crossAxisAlignment: WrapCrossAlignment.center, children: chips);
}
@@ -417,12 +498,28 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
children: [
for (var oi = 0; oi < q.options.length; oi++)
_optButton(qi, q.options[oi].label, q.options[oi].label, q.multiSelect, q.options[oi].description, oi + 1),
_optButton(qi, _kOther, 'Other…', q.multiSelect, '', q.options.length + 1),
_optButton(
qi,
_kOther,
ClideSettings.i18n.string(context, 'prompt.option.other', namespace: 'builtin.claude', placeholder: 'Other…'),
q.multiSelect,
'',
q.options.length + 1,
),
],
),
if (hasOther) ...[const SizedBox(height: 8), _NoteField(controller: _other[qi], placeholder: 'type your answer…')],
if (hasOther) ...[
const SizedBox(height: 8),
_NoteField(
controller: _other[qi],
placeholder: ClideSettings.i18n.string(context, 'prompt.other.placeholder', namespace: 'builtin.claude', placeholder: 'type your answer…'),
),
],
const SizedBox(height: 8),
_NoteField(controller: _qnote[qi], placeholder: '+ note (optional)'),
_NoteField(
controller: _qnote[qi],
placeholder: ClideSettings.i18n.string(context, 'prompt.note.placeholder', namespace: 'builtin.claude', placeholder: '+ note (optional)'),
),
],
);
}
@@ -452,7 +549,7 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
Widget _chatInstead(SurfaceTokens tokens) {
return ClideButton(
label: 'chat instead',
label: ClideSettings.i18n.string(context, 'prompt.chatInstead', namespace: 'builtin.claude', placeholder: 'chat instead'),
variant: ClideButtonVariant.subtle,
onPressed: () => widget.onResolve(widget.prompt.promptId, const DenyTool('User chose to chat instead.')),
);
@@ -491,15 +588,15 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
///
/// Shared between [ToolPromptCard] (permission prompt body) and the
/// [ConversationView] tool-use card bodies (T-168).
Widget toolInputBody(SurfaceTokens tokens, String toolName, Map<String, dynamic> input, String mono) {
Widget toolInputBody(BuildContext context, SurfaceTokens tokens, String toolName, Map<String, dynamic> input, String mono) {
switch (toolName) {
case 'Bash':
return toolBashBody(tokens, input);
return toolBashBody(context, tokens, input);
case 'Write':
return toolWriteBody(tokens, input, mono);
case 'Edit':
case 'MultiEdit':
return toolEditBody(tokens, input, mono);
return toolEditBody(context, tokens, input, mono);
case 'Read':
case 'Grep':
case 'LS':
@@ -511,9 +608,12 @@ Widget toolInputBody(SurfaceTokens tokens, String toolName, Map<String, dynamic>
/// Bash tool body: the command as a shell code block, with optional background
/// / timeout annotations.
Widget toolBashBody(SurfaceTokens tokens, Map<String, dynamic> input) {
Widget toolBashBody(BuildContext context, SurfaceTokens tokens, Map<String, dynamic> input) {
final cmd = (input['command'] as String? ?? '').trimRight();
final notes = <String>[if (input['run_in_background'] == true) 'background', if (input['timeout'] is num) 'timeout ${input['timeout']}ms'];
final notes = <String>[
if (input['run_in_background'] == true) ClideSettings.i18n.string(context, 'tool.bash.background', namespace: 'builtin.claude', placeholder: 'background'),
if (input['timeout'] is num) 'timeout ${input['timeout']}ms',
];
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
@@ -543,7 +643,7 @@ Widget toolWriteBody(SurfaceTokens tokens, Map<String, dynamic> input, String mo
}
/// Edit / MultiEdit tool body: before/after diff view.
Widget toolEditBody(SurfaceTokens tokens, Map<String, dynamic> input, String mono) {
Widget toolEditBody(BuildContext context, SurfaceTokens tokens, Map<String, dynamic> input, String mono) {
final path = input['file_path'] as String? ?? '';
final oldStr = input['old_string'] as String? ?? '';
final newStr = input['new_string'] as String? ?? '';
@@ -553,11 +653,21 @@ Widget toolEditBody(SurfaceTokens tokens, Map<String, dynamic> input, String mon
mainAxisSize: MainAxisSize.min,
children: [
if (path.isNotEmpty) toolPathLine(tokens, path, mono),
ClideText('— before', fontSize: clideFontMeta, color: tokens.globalTextMuted, fontFamily: mono),
ClideText(
ClideSettings.i18n.string(context, 'tool.edit.before', namespace: 'builtin.claude', placeholder: '— before'),
fontSize: clideFontMeta,
color: tokens.globalTextMuted,
fontFamily: mono,
),
const SizedBox(height: 4),
ClideCodeBlock(source: oldStr, language: lang),
const SizedBox(height: 8),
ClideText('+ after', fontSize: clideFontMeta, color: tokens.globalTextMuted, fontFamily: mono),
ClideText(
ClideSettings.i18n.string(context, 'tool.edit.after', namespace: 'builtin.claude', placeholder: '+ after'),
fontSize: clideFontMeta,
color: tokens.globalTextMuted,
fontFamily: mono,
),
const SizedBox(height: 4),
ClideCodeBlock(source: newStr, language: lang),
],
@@ -87,7 +87,7 @@ class _RunningIndicatorState extends State<RunningIndicator> with SingleTickerPr
final reduced = MediaQuery.maybeOf(context)?.disableAnimations ?? false;
// The animated text is decorative; AT gets one stable label.
return Semantics(
label: 'Claude is running',
label: ClideSettings.i18n.string(context, 'running.semantics', namespace: 'builtin.claude', placeholder: 'Claude is running'),
child: ExcludeSemantics(
child: reduced
? ClideText('${_verbs.first}', color: claudeAccent, fontSize: clideFontMeta)
+10 -2
View File
@@ -77,12 +77,20 @@ class _SessionPickerDialogState extends State<SessionPickerDialog> {
children: [
Padding(
padding: const EdgeInsets.fromLTRB(14, 12, 14, 8),
child: ClideText('Resume a Claude session', fontSize: clideFontBody, color: theme.globalForeground),
child: ClideText(
ClideSettings.i18n.string(context, 'sessionPicker.title', namespace: 'builtin.claude', placeholder: 'Resume a Claude session'),
fontSize: clideFontBody,
color: theme.globalForeground,
),
),
if (widget.sessions.isEmpty)
Padding(
padding: const EdgeInsets.fromLTRB(14, 4, 14, 16),
child: ClideText('No sessions found for this workspace.', muted: true, fontSize: clideFontSmall),
child: ClideText(
ClideSettings.i18n.string(context, 'sessionPicker.empty', namespace: 'builtin.claude', placeholder: 'No sessions found for this workspace.'),
muted: true,
fontSize: clideFontSmall,
),
)
else
Flexible(
+54 -10
View File
@@ -132,15 +132,19 @@ class _TeamChatSidebarState extends State<TeamChatSidebar> {
Row(
children: [
Expanded(
child: ClideText('MESSAGES', fontSize: clideFontSmall, color: tokens.globalTextMuted),
child: ClideText(
ClideSettings.i18n.string(context, 'teamChat.section.messages', namespace: 'builtin.claude', placeholder: 'MESSAGES'),
fontSize: clideFontSmall,
color: tokens.globalTextMuted,
),
),
Semantics(
button: true,
label: 'Open full chat pane',
label: ClideSettings.i18n.string(context, 'teamChat.popOut.semantics', namespace: 'builtin.claude', placeholder: 'Open full chat pane'),
excludeSemantics: true,
onTap: widget.onPopOut,
child: ClideTappable(
tooltip: 'Open full chat',
tooltip: ClideSettings.i18n.string(context, 'teamChat.popOut.tooltip', namespace: 'builtin.claude', placeholder: 'Open full chat'),
onTap: widget.onPopOut,
builder: (ctx, hovered, _) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 1),
@@ -155,7 +159,11 @@ class _TeamChatSidebarState extends State<TeamChatSidebar> {
if (messages.isEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 4),
child: ClideText('No messages yet.', muted: true, fontSize: clideFontSmall),
child: ClideText(
ClideSettings.i18n.string(context, 'teamChat.empty', namespace: 'builtin.claude', placeholder: 'No messages yet.'),
muted: true,
fontSize: clideFontSmall,
),
)
else
for (final msg in messages.length > 5 ? messages.sublist(messages.length - 5) : messages)
@@ -168,7 +176,13 @@ class _TeamChatSidebarState extends State<TeamChatSidebar> {
formatLabel: (n) => '@$n',
child: Focus(
onKeyEvent: _handleKeyEvent,
child: _ChatInputField(controller: _controller, focusNode: _focusNode, tokens: tokens, onSubmit: _submit, placeholder: '@name or @team …'),
child: _ChatInputField(
controller: _controller,
focusNode: _focusNode,
tokens: tokens,
onSubmit: _submit,
placeholder: ClideSettings.i18n.string(context, 'teamChat.composer.placeholder', namespace: 'builtin.claude', placeholder: '@name or @team …'),
),
),
),
],
@@ -303,12 +317,22 @@ class _TeamChatPaneState extends State<TeamChatPane> {
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: tokens.panelBorder)),
),
child: ClideText('Team Chat', fontSize: clideFontSmall, color: tokens.globalTextMuted),
child: ClideText(
ClideSettings.i18n.string(context, 'teamChat.pane.title', namespace: 'builtin.claude', placeholder: 'Team Chat'),
fontSize: clideFontSmall,
color: tokens.globalTextMuted,
),
),
// Timeline.
Expanded(
child: messages.isEmpty
? Center(child: ClideText('No messages yet.', muted: true, fontSize: clideFontSmall))
? Center(
child: ClideText(
ClideSettings.i18n.string(context, 'teamChat.empty', namespace: 'builtin.claude', placeholder: 'No messages yet.'),
muted: true,
fontSize: clideFontSmall,
),
)
: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
@@ -335,7 +359,12 @@ class _TeamChatPaneState extends State<TeamChatPane> {
children: [
Semantics(
checked: _interrupt,
label: 'Interrupt target session',
label: ClideSettings.i18n.string(
context,
'teamChat.interrupt.semantics',
namespace: 'builtin.claude',
placeholder: 'Interrupt target session',
),
excludeSemantics: true,
onTap: () => setState(() => _interrupt = !_interrupt),
child: Container(
@@ -350,7 +379,11 @@ class _TeamChatPaneState extends State<TeamChatPane> {
child: _interrupt ? Center(child: ClideIcon(PhosphorIcons.byName('check'), size: 9, color: tokens.globalFocus)) : null,
),
),
ClideText('Interrupt', fontSize: clideFontSmall, color: _interrupt ? tokens.globalForeground : tokens.globalTextMuted),
ClideText(
ClideSettings.i18n.string(context, 'teamChat.interrupt.label', namespace: 'builtin.claude', placeholder: 'Interrupt'),
fontSize: clideFontSmall,
color: _interrupt ? tokens.globalForeground : tokens.globalTextMuted,
),
],
),
),
@@ -363,7 +396,18 @@ class _TeamChatPaneState extends State<TeamChatPane> {
formatLabel: (n) => '@$n',
child: Focus(
onKeyEvent: _handleKeyEvent,
child: _ChatInputField(controller: _controller, focusNode: _focusNode, tokens: tokens, onSubmit: _submit, placeholder: '@name or @team …'),
child: _ChatInputField(
controller: _controller,
focusNode: _focusNode,
tokens: tokens,
onSubmit: _submit,
placeholder: ClideSettings.i18n.string(
context,
'teamChat.composer.placeholder',
namespace: 'builtin.claude',
placeholder: '@name or @team …',
),
),
),
),
],
@@ -3,5 +3,167 @@
"status.attaching": { "translation": "attaching…" },
"status.no-tmux": { "translation": "no-tmux · fresh every launch" },
"status.exited": { "translation": "session exited" },
"status.primary-exited": { "translation": "session exited — restart clide to retry" }
"status.primary-exited": { "translation": "session exited — restart clide to retry" },
"banner.title": { "translation": "Claude" },
"banner.warmingUp": { "translation": "Warming up — your conversation will appear here." },
"composer.hint": { "translation": "Message Claude… (Enter to send · Shift+Enter for newline)" },
"composer.stop": { "translation": "Stop ⎋" },
"composer.stop.hint": { "translation": "Interrupt the running turn (Escape)" },
"composer.removeAttachment": { "translation": "Remove {name}" },
"pane.starting": { "translation": "starting…" },
"pane.modeBadge.semantics": { "translation": "permission mode: {mode}" },
"running.semantics": { "translation": "Claude is running" },
"conversation.empty": { "translation": "Waiting for Claude…" },
"conversation.label.you": { "translation": "you" },
"conversation.label.claude": { "translation": "claude" },
"conversation.label.clide": { "translation": "clide" },
"conversation.label.agent": { "translation": "agent" },
"conversation.label.agentPrompt": { "translation": "agent prompt" },
"conversation.label.context": { "translation": "context" },
"conversation.label.thinking": { "translation": "thinking" },
"conversation.label.agentThinking": { "translation": "agent thinking" },
"conversation.label.image": { "translation": "image" },
"conversation.label.agentRun": { "translation": "agent run" },
"conversation.label.workflow": { "translation": "workflow" },
"conversation.label.error": { "translation": "error" },
"conversation.label.result": { "translation": "result" },
"conversation.label.denied": { "translation": "denied" },
"conversation.segment.prompt": { "translation": "prompt" },
"conversation.segment.result": { "translation": "result" },
"conversation.segment.liveTail": { "translation": "live tail" },
"conversation.segment.usage": { "translation": "usage" },
"conversation.segment.script": { "translation": "script" },
"conversation.workflow.launching": { "translation": "Launching…" },
"conversation.imagePlaceholder": { "translation": "could not load {path}" },
"conversation.bashTail.empty": { "translation": "no independent source to follow" },
"conversation.bashTail.label": { "translation": "live tail" },
"conversation.cluster.activity": { "translation": "Activity" },
"conversation.cluster.edits": { "translation": "Edits" },
"prompt.permission.allow": { "translation": "1. Allow" },
"prompt.permission.allowRemember": { "translation": "2. Allow & don't ask again" },
"prompt.permission.deny": { "translation": "{n}. Deny" },
"prompt.permission.denySimplify": { "translation": "{n}. Deny & simplify" },
"prompt.permission.denySimplify.tooltip": { "translation": "Deny and ask Claude to retry this action in a simpler format — complex interactions don't work well with the permission system." },
"prompt.permission.note.placeholder": { "translation": "add a note (optional) — sent to Claude" },
"prompt.permission.label": { "translation": "permission · {name}" },
"prompt.question.label": { "translation": "question" },
"prompt.review.label": { "translation": "review" },
"prompt.review.title": { "translation": "Review your answers" },
"prompt.submit": { "translation": "Submit" },
"prompt.submitAnswers": { "translation": "Submit answers" },
"prompt.back": { "translation": " Back" },
"prompt.next": { "translation": "Next " },
"prompt.reviewNav": { "translation": "Review " },
"prompt.nav.review": { "translation": "Review" },
"prompt.option.other": { "translation": "Other…" },
"prompt.other.placeholder": { "translation": "type your answer…" },
"prompt.note.placeholder": { "translation": "+ note (optional)" },
"prompt.chatInstead": { "translation": "chat instead" },
"tool.edit.before": { "translation": "— before" },
"tool.edit.after": { "translation": "+ after" },
"tool.bash.background": { "translation": "background" },
"permissionControl.semantics": { "translation": "permission mode: {mode}. Activate to change." },
"permissionControl.tooltip": { "translation": "Permission mode: {mode} — change (Ctrl/Cmd+M cycles)" },
"permissionBadge.tooltip": { "translation": "Permission mode: {mode}. Click to cycle default/acceptEdits/plan; Shift-click for bypassPermissions." },
"permissionBadge.semantics": { "translation": "Permission mode: {label}" },
"card.expand": { "translation": "Expand" },
"card.collapse": { "translation": "Collapse" },
"card.succeeded": { "translation": "succeeded" },
"card.failed": { "translation": "failed" },
"card.copy": { "translation": "copy" },
"taskDock.summary": { "translation": "{count} {tasks} · {done} done" },
"taskDock.task.singular": { "translation": "task" },
"taskDock.task.plural": { "translation": "tasks" },
"taskDock.collapse": { "translation": "Collapse tasks" },
"taskDock.expand": { "translation": "Expand tasks" },
"taskDock.semantics": { "translation": "Claude task list, {summary}, {state}" },
"taskDock.state.expanded": { "translation": "expanded" },
"taskDock.state.collapsed": { "translation": "collapsed" },
"taskDock.status.done": { "translation": "done" },
"taskDock.status.inProgress": { "translation": "in progress" },
"taskDock.status.pending": { "translation": "pending" },
"taskDock.row.semantics": { "translation": "{text}, {status}" },
"sessionPicker.title": { "translation": "Resume a Claude session" },
"sessionPicker.empty": { "translation": "No sessions found for this workspace." },
"modelPicker.cancel": { "translation": "cancel" },
"image.semantics": { "translation": "Image {name}" },
"activity.section.session": { "translation": "SESSION" },
"activity.control.clear": { "translation": "clear" },
"activity.control.compact": { "translation": "compact" },
"activity.control.fork": { "translation": "fork" },
"activity.control.resume": { "translation": "resume" },
"activity.control.refreshUsage": { "translation": "refresh usage" },
"activity.control.semantics": { "translation": "{label} session" },
"activity.control.tooltip": { "translation": "{label} · {command}" },
"activity.empty": { "translation": "No activity recorded yet." },
"activity.section.workflows": { "translation": "WORKFLOWS" },
"activity.workflow.fallback": { "translation": "workflow" },
"activity.workflow.done": { "translation": "done" },
"activity.workflow.starting": { "translation": "starting" },
"activity.section.usage": { "translation": "USAGE" },
"activity.row.session": { "translation": "session" },
"activity.row.weekAll": { "translation": "week (all)" },
"activity.row.weekSonnet": { "translation": "week (sonnet)" },
"activity.section.today": { "translation": "TODAY" },
"activity.row.messages": { "translation": "messages" },
"activity.row.sessions": { "translation": "sessions" },
"activity.row.toolCalls": { "translation": "tool calls" },
"activity.section.lifetime": { "translation": "LIFETIME" },
"activity.section.runtime": { "translation": "RUNTIME · primary" },
"activity.row.model": { "translation": "model" },
"activity.row.effort": { "translation": "effort" },
"activity.row.context": { "translation": "context" },
"activity.row.mode": { "translation": "mode" },
"activity.row.skills": { "translation": "skills" },
"config.empty": { "translation": "Claude environment not loaded." },
"config.section.settings": { "translation": "SETTINGS" },
"config.row.model": { "translation": "model" },
"config.row.effort": { "translation": "effort" },
"config.row.permissionMode": { "translation": "permission mode" },
"config.row.outputStyle": { "translation": "output style" },
"config.row.source": { "translation": "source" },
"config.row.source.value": { "translation": "~/.claude + .claude" },
"config.footer": { "translation": "expand a list to see all · click a skill/agent/command → opens its .md" },
"config.section.skills": { "translation": "SKILLS" },
"config.section.agents": { "translation": "AGENTS" },
"config.section.commands": { "translation": "COMMANDS" },
"config.section.hooks": { "translation": "HOOKS" },
"config.section.permissions": { "translation": "PERMISSIONS" },
"config.section.mcpServers": { "translation": "MCP SERVERS" },
"config.perm.allow": { "translation": "allow" },
"config.perm.ask": { "translation": "ask" },
"config.perm.deny": { "translation": "deny" },
"config.control.semantics": { "translation": "{label}: {value}. Click to change." },
"config.control.tooltip": { "translation": "change {label}" },
"config.control.option.semantics": { "translation": "{label}: {name}" },
"roster.bypass.confirmBody": { "translation": "Enable bypassPermissions? All tool calls will be auto-allowed." },
"roster.bypass.confirm.semantics": { "translation": "Confirm bypass" },
"roster.bypass.confirm.tooltip": { "translation": "Confirm" },
"roster.bypass.ok": { "translation": "OK" },
"roster.bypass.cancel.semantics": { "translation": "Cancel bypass" },
"roster.bypass.cancel.tooltip": { "translation": "Cancel" },
"roster.bypass.cancel": { "translation": "Cancel" },
"roster.hidePane": { "translation": "Hide pane" },
"roster.showPane": { "translation": "Show pane" },
"roster.unmute": { "translation": "Unmute messages" },
"roster.mute": { "translation": "Mute messages" },
"roster.inject": { "translation": "Inject message" },
"roster.fork": { "translation": "Fork session" },
"roster.close": { "translation": "Close session" },
"roster.inject.cancel": { "translation": "Cancel" },
"taskRow.reassign": { "translation": "Reassign task" },
"team.empty": { "translation": "No team active." },
"team.section.tasks": { "translation": "TASKS" },
"tabStrip.activity": { "translation": "Activity" },
"tabStrip.team": { "translation": "Team" },
"tabStrip.team.count": { "translation": "Team · {count}" },
"tabStrip.config": { "translation": "Config" },
"teamChat.section.messages": { "translation": "MESSAGES" },
"teamChat.popOut.semantics": { "translation": "Open full chat pane" },
"teamChat.popOut.tooltip": { "translation": "Open full chat" },
"teamChat.empty": { "translation": "No messages yet." },
"teamChat.composer.placeholder": { "translation": "@name or @team …" },
"teamChat.pane.title": { "translation": "Team Chat" },
"teamChat.interrupt.semantics": { "translation": "Interrupt target session" },
"teamChat.interrupt.label": { "translation": "Interrupt" }
}