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:
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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" }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user