chore: adopt Dart 3.9 toolchain — honest floor + tall-style reformat (T-353)
Raise the declared minimums in pubspec.yaml to what our deps already require: Flutter >=3.35.0 / Dart >=3.9.0 (was 3.19.0 / 3.5.0). alchemist 0.12 needs Flutter 3.32; Dart 3.9 first ships in Flutter 3.35, so 3.35 is the binding floor. Pin the exact build toolchain in .fvmrc (Flutter 3.44.1). Moving to the Dart 3.9 language level switches `dart format` to the new "tall" style and enables two new lints. This commit is the resulting mechanical churn, isolated from any behaviour change: - whole-tree `dart format` reformat (tall style) - `dart fix` for unnecessary_underscores + use_null_aware_elements No runtime behaviour change; `make test` green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -42,7 +42,8 @@ const List<String> clideAllowedToolsArgs = ['--allowedTools', clideBashAllowRule
|
||||
/// the orient-snapshot (`clide status`) and live pane/editor reflection
|
||||
/// arrive with Epic C (T-218..T-221) and are deliberately left out so the
|
||||
/// note never points the agent at a command that returns nothing yet.
|
||||
String clideContextNote(String workspaceRoot) => 'You are running inside clide, an IDE that is hosting this session. clide exposes its IDE '
|
||||
String clideContextNote(String workspaceRoot) =>
|
||||
'You are running inside clide, an IDE that is hosting this session. clide exposes its IDE '
|
||||
'surface as a `clide` command on your PATH; drive it with `clide <subsystem> <verb>`. '
|
||||
'Subsystems that respond today: `files` (workspace tree — `clide files root`, `files list`), '
|
||||
'`editor` (`clide editor open <path>`, `editor active`), `git` (`clide git status`), '
|
||||
@@ -61,16 +62,8 @@ String clideContextNote(String workspaceRoot) => 'You are running inside clide,
|
||||
/// * `CLIDE_WORKSPACE` — the workspace root.
|
||||
/// * `PATH` — prepended with [clideCliDir] when it is non-null (i.e. `clide`
|
||||
/// is not already resolvable), otherwise left untouched.
|
||||
Map<String, String> agentEnvDelta({
|
||||
required String workspaceRoot,
|
||||
required String socketPath,
|
||||
required String? currentPath,
|
||||
required String? clideCliDir,
|
||||
}) {
|
||||
final delta = <String, String>{
|
||||
'CLIDE_SOCK': socketPath,
|
||||
'CLIDE_WORKSPACE': workspaceRoot,
|
||||
};
|
||||
Map<String, String> agentEnvDelta({required String workspaceRoot, required String socketPath, required String? currentPath, required String? clideCliDir}) {
|
||||
final delta = <String, String>{'CLIDE_SOCK': socketPath, 'CLIDE_WORKSPACE': workspaceRoot};
|
||||
if (clideCliDir != null && clideCliDir.isNotEmpty) {
|
||||
delta['PATH'] = (currentPath == null || currentPath.isEmpty) ? clideCliDir : '$clideCliDir:$currentPath';
|
||||
}
|
||||
@@ -85,11 +78,7 @@ Map<String, String> agentEnvDelta({
|
||||
///
|
||||
/// [candidateDirs] is an ordered fallback list; [isExecutableFile] probes
|
||||
/// `<dir>/clide`. Both are injected so the resolver is pure and testable.
|
||||
String? resolveClideCliDir({
|
||||
required String? currentPath,
|
||||
required List<String> candidateDirs,
|
||||
required bool Function(String path) isExecutableFile,
|
||||
}) {
|
||||
String? resolveClideCliDir({required String? currentPath, required List<String> candidateDirs, required bool Function(String path) isExecutableFile}) {
|
||||
if (currentPath != null) {
|
||||
for (final dir in currentPath.split(':')) {
|
||||
if (dir.isNotEmpty && isExecutableFile('$dir/clide')) return null;
|
||||
@@ -142,21 +131,9 @@ AgentBootstrap agentBootstrap(String workspaceRoot, {Map<String, String>? base})
|
||||
'$workspaceRoot/native/${nativeClideDirName()}',
|
||||
File(Platform.resolvedExecutable).parent.path,
|
||||
];
|
||||
final cliDir = resolveClideCliDir(
|
||||
currentPath: currentPath,
|
||||
candidateDirs: candidates,
|
||||
isExecutableFile: _isExecutableFile,
|
||||
);
|
||||
final delta = agentEnvDelta(
|
||||
workspaceRoot: workspaceRoot,
|
||||
socketPath: workspaceSocketPath(workspaceRoot),
|
||||
currentPath: currentPath,
|
||||
clideCliDir: cliDir,
|
||||
);
|
||||
return AgentBootstrap(
|
||||
envDelta: {...?base, ...delta},
|
||||
extraArgs: ['--allowedTools', clideBashAllowRule],
|
||||
);
|
||||
final cliDir = resolveClideCliDir(currentPath: currentPath, candidateDirs: candidates, isExecutableFile: _isExecutableFile);
|
||||
final delta = agentEnvDelta(workspaceRoot: workspaceRoot, socketPath: workspaceSocketPath(workspaceRoot), currentPath: currentPath, clideCliDir: cliDir);
|
||||
return AgentBootstrap(envDelta: {...?base, ...delta}, extraArgs: ['--allowedTools', clideBashAllowRule]);
|
||||
}
|
||||
|
||||
bool _isExecutableFile(String path) {
|
||||
|
||||
@@ -35,20 +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('Claude', fontSize: clideFontDialogTitle, color: claudeAccent, fontWeight: FontWeight.w500),
|
||||
const SizedBox(height: 2),
|
||||
ClideText(role, fontSize: clideFontSmall, muted: true, fontFamily: clideMonoFamily),
|
||||
const SizedBox(height: 16),
|
||||
if (ws != null) ClideText(ws, fontSize: clideFontCaption, muted: true),
|
||||
if (statusLine != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
ClideText(statusLine!, fontSize: clideFontSmall, muted: true, fontFamily: clideMonoFamily),
|
||||
],
|
||||
if (statusLine != null) ...[const SizedBox(height: 2), ClideText(statusLine!, fontSize: clideFontSmall, muted: true, fontFamily: clideMonoFamily)],
|
||||
const SizedBox(height: 16),
|
||||
const ClideText('Warming up — your conversation will appear here.', fontSize: clideFontSmall, muted: true),
|
||||
],
|
||||
|
||||
@@ -352,7 +352,12 @@ class _ClaudeComposerState extends State<ClaudeComposer> {
|
||||
}
|
||||
}
|
||||
|
||||
void _applyHistory(String text) => _applyValue(TextEditingValue(text: text, selection: TextSelection.collapsed(offset: text.length)));
|
||||
void _applyHistory(String text) => _applyValue(
|
||||
TextEditingValue(
|
||||
text: text,
|
||||
selection: TextSelection.collapsed(offset: text.length),
|
||||
),
|
||||
);
|
||||
|
||||
/// Set the controller without it being treated as a user edit (so the
|
||||
/// preview doesn't overwrite the persisted draft or exit navigation).
|
||||
@@ -368,10 +373,7 @@ class _ClaudeComposerState extends State<ClaudeComposer> {
|
||||
final tokens = _attachments.map((a) => a.pathToken);
|
||||
if (text.trim().isEmpty && _attachments.isEmpty) return;
|
||||
// Typed text first, then the attachment @path references.
|
||||
final message = [
|
||||
if (text.trim().isNotEmpty) text,
|
||||
...tokens,
|
||||
].join(' ');
|
||||
final message = [if (text.trim().isNotEmpty) text, ...tokens].join(' ');
|
||||
widget.onSubmit(message);
|
||||
_controller.clear();
|
||||
setState(() => _attachments.clear());
|
||||
@@ -454,11 +456,7 @@ 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(theme, a)]),
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
@@ -489,13 +487,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(widget.hint, muted: true, fontSize: clideFontBody)),
|
||||
EditableText(
|
||||
controller: _controller,
|
||||
focusNode: _focus,
|
||||
@@ -516,10 +508,7 @@ class _ClaudeComposerState extends State<ClaudeComposer> {
|
||||
const SizedBox(width: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: PermissionModeControl(
|
||||
mode: widget.permissionMode!,
|
||||
onSelect: widget.onSetPermissionMode!,
|
||||
),
|
||||
child: PermissionModeControl(mode: widget.permissionMode!, onSelect: widget.onSetPermissionMode!),
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -548,12 +537,7 @@ class _ClaudeComposerState extends State<ClaudeComposer> {
|
||||
_chipLeading(theme, a),
|
||||
const SizedBox(width: 6),
|
||||
Flexible(
|
||||
child: ClideText(
|
||||
a.fileName,
|
||||
fontSize: clideFontSmall,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
child: ClideText(a.fileName, fontSize: clideFontSmall, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Semantics(
|
||||
|
||||
@@ -94,13 +94,7 @@ class ClaudePermissions {
|
||||
/// + plugin + MCP), the skill names, the default model and permission mode.
|
||||
@immutable
|
||||
class ClaudeProbe {
|
||||
const ClaudeProbe({
|
||||
required this.version,
|
||||
required this.slashCommands,
|
||||
required this.skills,
|
||||
this.model,
|
||||
this.permissionMode,
|
||||
});
|
||||
const ClaudeProbe({required this.version, required this.slashCommands, required this.skills, this.model, this.permissionMode});
|
||||
|
||||
final String version;
|
||||
final List<String> slashCommands;
|
||||
@@ -109,12 +103,12 @@ class ClaudeProbe {
|
||||
final String? permissionMode;
|
||||
|
||||
Map<String, Object?> toJson() => {
|
||||
'version': version,
|
||||
'slash_commands': slashCommands,
|
||||
'skills': skills,
|
||||
if (model != null) 'model': model,
|
||||
if (permissionMode != null) 'permission_mode': permissionMode,
|
||||
};
|
||||
'version': version,
|
||||
'slash_commands': slashCommands,
|
||||
'skills': skills,
|
||||
if (model != null) 'model': model,
|
||||
if (permissionMode != null) 'permission_mode': permissionMode,
|
||||
};
|
||||
|
||||
/// Build from a stream-json `init` event object. Returns null if it doesn't
|
||||
/// look like an init event (no version field).
|
||||
@@ -130,12 +124,12 @@ class ClaudeProbe {
|
||||
}
|
||||
|
||||
static ClaudeProbe fromCache(Map<String, Object?> j) => ClaudeProbe(
|
||||
version: (j['version'] as String?) ?? '',
|
||||
slashCommands: _stringList(j['slash_commands']),
|
||||
skills: _stringList(j['skills']),
|
||||
model: j['model'] as String?,
|
||||
permissionMode: j['permission_mode'] as String?,
|
||||
);
|
||||
version: (j['version'] as String?) ?? '',
|
||||
slashCommands: _stringList(j['slash_commands']),
|
||||
skills: _stringList(j['skills']),
|
||||
model: j['model'] as String?,
|
||||
permissionMode: j['permission_mode'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
List<String> _stringList(Object? v) => v is List ? v.whereType<String>().toList(growable: false) : const [];
|
||||
@@ -184,13 +178,13 @@ class ClaudeConfig extends ChangeNotifier {
|
||||
ClaudeInitProbe? initProbe,
|
||||
ClaudeConfigWatch? watch,
|
||||
Duration debounce = const Duration(milliseconds: 150),
|
||||
}) : _globalDir = globalDir,
|
||||
_cacheDir = cacheDir,
|
||||
_projectDir = projectDir,
|
||||
_versionRunner = versionRunner ?? _defaultVersionRunner,
|
||||
_initProbe = initProbe ?? _defaultInitProbe,
|
||||
_watch = watch,
|
||||
_debounceFor = debounce;
|
||||
}) : _globalDir = globalDir,
|
||||
_cacheDir = cacheDir,
|
||||
_projectDir = projectDir,
|
||||
_versionRunner = versionRunner ?? _defaultVersionRunner,
|
||||
_initProbe = initProbe ?? _defaultInitProbe,
|
||||
_watch = watch,
|
||||
_debounceFor = debounce;
|
||||
|
||||
final Directory _globalDir;
|
||||
final Directory _cacheDir;
|
||||
@@ -400,10 +394,7 @@ class ClaudeConfig extends ChangeNotifier {
|
||||
/// Global first so that local entries, added later, win on collisions.
|
||||
List<(ConfigScope, Directory)> _scopeDirs() {
|
||||
final pd = _projectDir;
|
||||
return [
|
||||
(ConfigScope.global, _globalDir),
|
||||
if (pd != null) (ConfigScope.local, Directory('${pd.path}/.claude')),
|
||||
];
|
||||
return [(ConfigScope.global, _globalDir), if (pd != null) (ConfigScope.local, Directory('${pd.path}/.claude'))];
|
||||
}
|
||||
|
||||
Future<List<ClaudeSkill>> _loadSkills(Directory scopeDir, ConfigScope scope) async {
|
||||
@@ -415,12 +406,7 @@ class ClaudeConfig extends ChangeNotifier {
|
||||
final manifest = File('${entry.path}/SKILL.md');
|
||||
if (!await manifest.exists()) continue;
|
||||
final fm = _parseFrontmatter(await manifest.readAsString());
|
||||
out.add(ClaudeSkill(
|
||||
name: fm.name ?? _basename(entry.path),
|
||||
description: fm.description,
|
||||
scope: scope,
|
||||
path: manifest.path,
|
||||
));
|
||||
out.add(ClaudeSkill(name: fm.name ?? _basename(entry.path), description: fm.description, scope: scope, path: manifest.path));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -432,11 +418,7 @@ class ClaudeConfig extends ChangeNotifier {
|
||||
await for (final entry in dir.list()) {
|
||||
if (entry is! File || !entry.path.endsWith('.md')) continue;
|
||||
final base = _basename(entry.path);
|
||||
out.add(ClaudeCommand(
|
||||
name: base.substring(0, base.length - 3),
|
||||
scope: scope,
|
||||
path: entry.path,
|
||||
));
|
||||
out.add(ClaudeCommand(name: base.substring(0, base.length - 3), scope: scope, path: entry.path));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -449,11 +431,7 @@ class ClaudeConfig extends ChangeNotifier {
|
||||
await for (final entry in dir.list()) {
|
||||
if (entry is! File || !entry.path.endsWith('.md')) continue;
|
||||
final base = _basename(entry.path);
|
||||
out.add(ClaudeAgent(
|
||||
name: base.substring(0, base.length - 3),
|
||||
scope: scope,
|
||||
path: entry.path,
|
||||
));
|
||||
out.add(ClaudeAgent(name: base.substring(0, base.length - 3), scope: scope, path: entry.path));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -472,11 +450,7 @@ class ClaudeConfig extends ChangeNotifier {
|
||||
ClaudePermissions _permissionsOf(Map<String, Object?> settings) {
|
||||
final p = settings['permissions'];
|
||||
if (p is! Map) return const ClaudePermissions();
|
||||
return ClaudePermissions(
|
||||
allow: _stringList(p['allow']),
|
||||
deny: _stringList(p['deny']),
|
||||
ask: _stringList(p['ask']),
|
||||
);
|
||||
return ClaudePermissions(allow: _stringList(p['allow']), deny: _stringList(p['deny']), ask: _stringList(p['ask']));
|
||||
}
|
||||
|
||||
// ---- Watching -----------------------------------------------------------
|
||||
@@ -573,9 +547,7 @@ class ClaudeConfig extends ChangeNotifier {
|
||||
/// Claude's `mcpServers` is a `Map<String, {...config}>` keyed on server name.
|
||||
static List<ClaudeMcpServer> _parseMcpServers(Object? raw) {
|
||||
if (raw is! Map) return const [];
|
||||
return [
|
||||
for (final key in raw.keys) ClaudeMcpServer(name: '$key'),
|
||||
];
|
||||
return [for (final key in raw.keys) ClaudeMcpServer(name: '$key')];
|
||||
}
|
||||
|
||||
static List<T> _dedupeByName<T>(List<T> all, String Function(T) nameOf) {
|
||||
@@ -608,14 +580,7 @@ Future<String?> _defaultVersionRunner() async {
|
||||
|
||||
Future<String?> _defaultInitProbe() async {
|
||||
try {
|
||||
final r = await Process.run('claude', [
|
||||
'-p',
|
||||
'.',
|
||||
'--no-session-persistence',
|
||||
'--output-format',
|
||||
'stream-json',
|
||||
'--verbose',
|
||||
]);
|
||||
final r = await Process.run('claude', ['-p', '.', '--no-session-persistence', '--output-format', 'stream-json', '--verbose']);
|
||||
return r.stdout as String?;
|
||||
} catch (_) {
|
||||
return null;
|
||||
|
||||
@@ -218,14 +218,18 @@ class _ClaudeMetaSidebarState extends State<ClaudeMetaSidebar> {
|
||||
final managed = orch.byMemberName(memberName);
|
||||
if (managed == null) return;
|
||||
final forkId = 'fork:$memberName-${DateTime.now().millisecondsSinceEpoch}';
|
||||
unawaited(orch.spawn(SpawnSpec(
|
||||
id: forkId,
|
||||
role: 'fork of $memberName',
|
||||
// sessionId is a placeholder; real claude session id arrives via init.
|
||||
sessionId: forkId,
|
||||
cwd: managed.cwd,
|
||||
forkSourceSessionId: managed.sessionId,
|
||||
)));
|
||||
unawaited(
|
||||
orch.spawn(
|
||||
SpawnSpec(
|
||||
id: forkId,
|
||||
role: 'fork of $memberName',
|
||||
// sessionId is a placeholder; real claude session id arrives via init.
|
||||
sessionId: forkId,
|
||||
cwd: managed.cwd,
|
||||
forkSourceSessionId: managed.sessionId,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _refreshStats() async {
|
||||
@@ -276,11 +280,7 @@ class _ClaudeMetaSidebarState extends State<ClaudeMetaSidebar> {
|
||||
_MetaRow('sessions', '${latest.sessionCount}'),
|
||||
_MetaRow('tool calls', '${latest.toolCallCount}'),
|
||||
]),
|
||||
if (latest != null)
|
||||
_MetaSection('LIFETIME', [
|
||||
_MetaRow('messages', '${_stats.lifetimeMessages}'),
|
||||
_MetaRow('sessions', '${_stats.lifetimeSessions}'),
|
||||
]),
|
||||
if (latest != null) _MetaSection('LIFETIME', [_MetaRow('messages', '${_stats.lifetimeMessages}'), _MetaRow('sessions', '${_stats.lifetimeSessions}')]),
|
||||
..._runtimeSection(tokens),
|
||||
];
|
||||
if (sections.isEmpty) {
|
||||
@@ -357,17 +357,10 @@ class _ClaudeMetaSidebarState extends State<ClaudeMetaSidebar> {
|
||||
final broker = _orchestrator?.broker;
|
||||
if (chatModel != null && broker != null) {
|
||||
children.add(const SizedBox(height: 12));
|
||||
children.add(TeamChatSidebar(
|
||||
model: chatModel,
|
||||
broker: broker,
|
||||
onPopOut: _openChatPane,
|
||||
));
|
||||
children.add(TeamChatSidebar(model: chatModel, broker: broker, onPopOut: _openChatPane));
|
||||
}
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(12),
|
||||
children: children,
|
||||
);
|
||||
return ListView(padding: const EdgeInsets.all(12), children: children);
|
||||
}
|
||||
|
||||
Widget _taskSection(SurfaceTokens tokens) {
|
||||
@@ -420,18 +413,11 @@ class _ClaudeMetaSidebarState extends State<ClaudeMetaSidebar> {
|
||||
// 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('expand a list to see all · click a skill/agent/command → opens its .md', muted: true, fontSize: clideFontSmall),
|
||||
),
|
||||
];
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(12),
|
||||
children: children,
|
||||
);
|
||||
return ListView(padding: const EdgeInsets.all(12), children: children);
|
||||
}
|
||||
|
||||
/// One key→value row in the pinned SETTINGS table.
|
||||
@@ -454,22 +440,22 @@ class _ClaudeMetaSidebarState extends State<ClaudeMetaSidebar> {
|
||||
}
|
||||
|
||||
String _configSectionLabel(_ConfigSection section) => switch (section) {
|
||||
_ConfigSection.skills => 'SKILLS',
|
||||
_ConfigSection.agents => 'AGENTS',
|
||||
_ConfigSection.commands => 'COMMANDS',
|
||||
_ConfigSection.hooks => 'HOOKS',
|
||||
_ConfigSection.permissions => 'PERMISSIONS',
|
||||
_ConfigSection.mcpServers => 'MCP SERVERS',
|
||||
};
|
||||
_ConfigSection.skills => 'SKILLS',
|
||||
_ConfigSection.agents => 'AGENTS',
|
||||
_ConfigSection.commands => 'COMMANDS',
|
||||
_ConfigSection.hooks => 'HOOKS',
|
||||
_ConfigSection.permissions => 'PERMISSIONS',
|
||||
_ConfigSection.mcpServers => 'MCP SERVERS',
|
||||
};
|
||||
|
||||
int _configSectionCount(ClaudeConfig config, _ConfigSection section) => switch (section) {
|
||||
_ConfigSection.skills => config.skills.length,
|
||||
_ConfigSection.agents => config.agents.length,
|
||||
_ConfigSection.commands => config.commands.length,
|
||||
_ConfigSection.hooks => config.hooks.length,
|
||||
_ConfigSection.permissions => config.permissions.allow.length + config.permissions.deny.length + config.permissions.ask.length,
|
||||
_ConfigSection.mcpServers => config.mcpServers.length,
|
||||
};
|
||||
_ConfigSection.skills => config.skills.length,
|
||||
_ConfigSection.agents => config.agents.length,
|
||||
_ConfigSection.commands => config.commands.length,
|
||||
_ConfigSection.hooks => config.hooks.length,
|
||||
_ConfigSection.permissions => config.permissions.allow.length + config.permissions.deny.length + config.permissions.ask.length,
|
||||
_ConfigSection.mcpServers => config.mcpServers.length,
|
||||
};
|
||||
|
||||
Widget _configAccordion(SurfaceTokens tokens, ClaudeConfig config, _ConfigSection section) {
|
||||
final expanded = _expanded.contains(section);
|
||||
@@ -492,17 +478,11 @@ class _ClaudeMetaSidebarState extends State<ClaudeMetaSidebar> {
|
||||
List<Widget> _configSectionChildren(SurfaceTokens tokens, ClaudeConfig config, _ConfigSection section) {
|
||||
switch (section) {
|
||||
case _ConfigSection.skills:
|
||||
return [
|
||||
for (final skill in config.skills) _configFileRow(tokens, skill.name, skill.path),
|
||||
];
|
||||
return [for (final skill in config.skills) _configFileRow(tokens, skill.name, skill.path)];
|
||||
case _ConfigSection.agents:
|
||||
return [
|
||||
for (final agent in config.agents) _configFileRow(tokens, agent.name, agent.path),
|
||||
];
|
||||
return [for (final agent in config.agents) _configFileRow(tokens, agent.name, agent.path)];
|
||||
case _ConfigSection.commands:
|
||||
return [
|
||||
for (final cmd in config.commands) _configFileRow(tokens, cmd.name, cmd.path),
|
||||
];
|
||||
return [for (final cmd in config.commands) _configFileRow(tokens, cmd.name, cmd.path)];
|
||||
case _ConfigSection.hooks:
|
||||
return [
|
||||
for (final hook in config.hooks)
|
||||
@@ -540,11 +520,7 @@ class _ClaudeMetaSidebarState extends State<ClaudeMetaSidebar> {
|
||||
Widget _configFileRow(SurfaceTokens tokens, String name, String? path) {
|
||||
final row = Padding(
|
||||
padding: const EdgeInsets.only(left: 16, top: 2, bottom: 2),
|
||||
child: ClideText(
|
||||
name,
|
||||
fontSize: clideFontSmall,
|
||||
color: path != null ? tokens.globalFocus : tokens.globalForeground,
|
||||
),
|
||||
child: ClideText(name, fontSize: clideFontSmall, color: path != null ? tokens.globalFocus : tokens.globalForeground),
|
||||
);
|
||||
if (path == null) return row;
|
||||
void openMarkdown() => ClideKernel.of(context).messages.publish('builtin.markdown', 'selection', {'path': path});
|
||||
@@ -558,11 +534,7 @@ class _ClaudeMetaSidebarState extends State<ClaudeMetaSidebar> {
|
||||
onTap: openMarkdown,
|
||||
builder: (ctx, hovered, _) => Padding(
|
||||
padding: const EdgeInsets.only(left: 16, top: 2, bottom: 2),
|
||||
child: ClideText(
|
||||
name,
|
||||
fontSize: clideFontSmall,
|
||||
color: hovered ? tokens.globalForeground : tokens.globalFocus,
|
||||
),
|
||||
child: ClideText(name, fontSize: clideFontSmall, color: hovered ? tokens.globalForeground : tokens.globalFocus),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -576,22 +548,18 @@ class _ClaudeMetaSidebarState extends State<ClaudeMetaSidebar> {
|
||||
|
||||
// allow → statusSuccess, ask → statusWarning, deny → statusError
|
||||
Color kindColor(_ConfigPermKind k) => switch (k) {
|
||||
_ConfigPermKind.allow => tokens.statusSuccess,
|
||||
_ConfigPermKind.ask => tokens.statusWarning,
|
||||
_ConfigPermKind.deny => tokens.statusError,
|
||||
};
|
||||
_ConfigPermKind.allow => tokens.statusSuccess,
|
||||
_ConfigPermKind.ask => tokens.statusWarning,
|
||||
_ConfigPermKind.deny => tokens.statusError,
|
||||
};
|
||||
|
||||
String kindLabel(_ConfigPermKind k) => switch (k) {
|
||||
_ConfigPermKind.allow => kindAllow,
|
||||
_ConfigPermKind.ask => kindAsk,
|
||||
_ConfigPermKind.deny => kindDeny,
|
||||
};
|
||||
_ConfigPermKind.allow => kindAllow,
|
||||
_ConfigPermKind.ask => kindAsk,
|
||||
_ConfigPermKind.deny => kindDeny,
|
||||
};
|
||||
|
||||
final groups = [
|
||||
(_ConfigPermKind.allow, perms.allow),
|
||||
(_ConfigPermKind.ask, perms.ask),
|
||||
(_ConfigPermKind.deny, perms.deny),
|
||||
];
|
||||
final groups = [(_ConfigPermKind.allow, perms.allow), (_ConfigPermKind.ask, perms.ask), (_ConfigPermKind.deny, perms.deny)];
|
||||
|
||||
final rows = <Widget>[];
|
||||
for (final (kind, rules) in groups) {
|
||||
@@ -631,44 +599,41 @@ class _ClaudeMetaSidebarState extends State<ClaudeMetaSidebar> {
|
||||
// --- Shared rendering -----------------------------------------------------
|
||||
|
||||
Widget _placeholder(String text) => Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: ClideText(text, muted: true, fontSize: clideFontSmall),
|
||||
);
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: ClideText(text, muted: true, fontSize: clideFontSmall),
|
||||
);
|
||||
|
||||
Widget _metaTable(SurfaceTokens tokens, List<_MetaSection> sections) {
|
||||
final children = <Widget>[];
|
||||
for (var i = 0; i < sections.length; i++) {
|
||||
final s = sections[i];
|
||||
children.add(Padding(
|
||||
padding: EdgeInsets.only(top: i == 0 ? 0 : 16, bottom: 6),
|
||||
child: ClideText(s.header, fontSize: clideFontSmall, color: tokens.globalTextMuted),
|
||||
));
|
||||
children.add(
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: i == 0 ? 0 : 16, bottom: 6),
|
||||
child: ClideText(s.header, fontSize: clideFontSmall, color: tokens.globalTextMuted),
|
||||
),
|
||||
);
|
||||
for (final r in s.rows) {
|
||||
children.add(Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: _rowPitch),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: _labelColumnWidth,
|
||||
child: ClideText(r.label, muted: true, fontSize: clideFontSmall),
|
||||
),
|
||||
Expanded(
|
||||
child: ClideText(
|
||||
r.value,
|
||||
fontSize: clideFontSmall,
|
||||
color: r.valueColor ?? tokens.globalForeground,
|
||||
children.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: _rowPitch),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: _labelColumnWidth,
|
||||
child: ClideText(r.label, muted: true, fontSize: clideFontSmall),
|
||||
),
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
child: ClideText(r.value, fontSize: clideFontSmall, color: r.valueColor ?? tokens.globalForeground),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
}
|
||||
}
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(12),
|
||||
children: children,
|
||||
);
|
||||
return ListView(padding: const EdgeInsets.all(12), children: children);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -788,7 +753,11 @@ class _AgentRosterRowState extends State<_AgentRosterRow> {
|
||||
// Color dot
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 3),
|
||||
child: Container(width: 8, height: 8, decoration: BoxDecoration(color: color, shape: BoxShape.circle)),
|
||||
child: Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Name + status
|
||||
@@ -840,11 +809,7 @@ 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('Enable bypassPermissions? All tool calls will be auto-allowed.', fontSize: clideFontSmall, color: tokens.globalTextMuted),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
// Confirm
|
||||
@@ -889,14 +854,7 @@ class _AgentRosterRowState extends State<_AgentRosterRow> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildControls(
|
||||
BuildContext context,
|
||||
SurfaceTokens tokens,
|
||||
ManagedSession managed,
|
||||
bool isVisible,
|
||||
bool isMuted,
|
||||
bool isInjecting,
|
||||
) {
|
||||
Widget _buildControls(BuildContext context, SurfaceTokens tokens, ManagedSession managed, bool isVisible, bool isMuted, bool isInjecting) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -975,11 +933,11 @@ class _AgentRosterRowState extends State<_AgentRosterRow> {
|
||||
|
||||
/// Maps a permission-mode string to a single-letter badge label.
|
||||
String _permissionModeBadge(String mode) => switch (mode) {
|
||||
'acceptEdits' => 'A',
|
||||
'plan' => 'P',
|
||||
'bypassPermissions' => 'B',
|
||||
_ => 'D', // default
|
||||
};
|
||||
'acceptEdits' => 'A',
|
||||
'plan' => 'P',
|
||||
'bypassPermissions' => 'B',
|
||||
_ => 'D', // default
|
||||
};
|
||||
|
||||
/// Clickable permission-mode badge shown in each roster row (T-181).
|
||||
///
|
||||
@@ -990,12 +948,7 @@ String _permissionModeBadge(String mode) => switch (mode) {
|
||||
/// It is a custom painted label (no Material), consistent with the rendering
|
||||
/// stack rules (D-7, CLAUDE.md guardrails).
|
||||
class _PermissionModeBadge extends StatelessWidget {
|
||||
const _PermissionModeBadge({
|
||||
required this.mode,
|
||||
required this.tokens,
|
||||
required this.onCycle,
|
||||
required this.onBypass,
|
||||
});
|
||||
const _PermissionModeBadge({required this.mode, required this.tokens, required this.onCycle, required this.onBypass});
|
||||
|
||||
final String mode;
|
||||
final SurfaceTokens tokens;
|
||||
@@ -1012,7 +965,8 @@ class _PermissionModeBadge extends StatelessWidget {
|
||||
final isBypass = mode == 'bypassPermissions';
|
||||
final badgeColor = isBypass ? const Color(0xFFF06C6F) : tokens.globalFocus;
|
||||
|
||||
final tooltip = 'Permission mode: ${permissionModeLabel(mode)}. '
|
||||
final tooltip =
|
||||
'Permission mode: ${permissionModeLabel(mode)}. '
|
||||
'Click to cycle default/acceptEdits/plan; Shift-click for bypassPermissions.';
|
||||
|
||||
return Padding(
|
||||
@@ -1046,11 +1000,7 @@ class _PermissionModeBadge extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
border: Border.all(color: badgeColor.withAlpha(hovered ? 180 : 100), width: 1),
|
||||
),
|
||||
child: ClideText(
|
||||
label,
|
||||
fontSize: 9,
|
||||
color: badgeColor,
|
||||
),
|
||||
child: ClideText(label, fontSize: 9, color: badgeColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1060,12 +1010,7 @@ class _PermissionModeBadge extends StatelessWidget {
|
||||
|
||||
/// A single icon-button used in the roster row controls.
|
||||
class _IconButton extends StatelessWidget {
|
||||
const _IconButton({
|
||||
required this.painter,
|
||||
required this.tooltip,
|
||||
required this.color,
|
||||
required this.onTap,
|
||||
});
|
||||
const _IconButton({required this.painter, required this.tooltip, required this.color, required this.onTap});
|
||||
|
||||
final ClideIconPainter painter;
|
||||
final String tooltip;
|
||||
@@ -1096,11 +1041,7 @@ class _IconButton extends StatelessWidget {
|
||||
/// Inline text input for injecting a message into a session (T-171).
|
||||
/// Submits on Enter; Cancel is handled by the parent via [_IconButton].
|
||||
class _InjectTextField extends StatelessWidget {
|
||||
const _InjectTextField({
|
||||
required this.controller,
|
||||
required this.tokens,
|
||||
required this.onSubmit,
|
||||
});
|
||||
const _InjectTextField({required this.controller, required this.tokens, required this.onSubmit});
|
||||
|
||||
final TextEditingController controller;
|
||||
final SurfaceTokens tokens;
|
||||
@@ -1119,12 +1060,7 @@ class _InjectTextField extends StatelessWidget {
|
||||
child: EditableText(
|
||||
controller: controller,
|
||||
focusNode: FocusNode(debugLabel: 'inject-${controller.hashCode}')..requestFocus(),
|
||||
style: TextStyle(
|
||||
fontFamily: 'JetBrains Mono',
|
||||
fontSize: clideFontSmall,
|
||||
color: tokens.globalForeground,
|
||||
height: 1.4,
|
||||
),
|
||||
style: TextStyle(fontFamily: 'JetBrains Mono', fontSize: clideFontSmall, color: tokens.globalForeground, height: 1.4),
|
||||
cursorColor: tokens.globalFocus,
|
||||
backgroundCursorColor: tokens.globalTextMuted,
|
||||
onSubmitted: onSubmit,
|
||||
@@ -1139,11 +1075,7 @@ class _InjectTextField extends StatelessWidget {
|
||||
|
||||
/// One row in the TASKS section: status marker + title + owner + reassign.
|
||||
class _TaskRow extends StatelessWidget {
|
||||
const _TaskRow({
|
||||
required this.task,
|
||||
required this.members,
|
||||
required this.broker,
|
||||
});
|
||||
const _TaskRow({required this.task, required this.members, required this.broker});
|
||||
|
||||
final TeamTask task;
|
||||
final List<TeamMemberJoined> members;
|
||||
@@ -1240,18 +1172,9 @@ class _TabStrip extends StatelessWidget {
|
||||
builder: (ctx, hovered, _) => Container(
|
||||
padding: const EdgeInsets.only(bottom: 3),
|
||||
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,
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1262,8 +1185,8 @@ class _TabStrip extends StatelessWidget {
|
||||
}
|
||||
|
||||
String _label(SidebarTab t) => switch (t) {
|
||||
SidebarTab.activity => 'Activity',
|
||||
SidebarTab.team => memberCount == 0 ? 'Team' : 'Team · $memberCount',
|
||||
SidebarTab.config => 'Config',
|
||||
};
|
||||
SidebarTab.activity => 'Activity',
|
||||
SidebarTab.team => memberCount == 0 ? 'Team' : 'Team · $memberCount',
|
||||
SidebarTab.config => 'Config',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -197,9 +197,12 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
sub.cancel();
|
||||
if (!c.isCompleted) c.complete();
|
||||
});
|
||||
await c.future.timeout(const Duration(seconds: 10), onTimeout: () {
|
||||
sub.cancel();
|
||||
});
|
||||
await c.future.timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () {
|
||||
sub.cancel();
|
||||
},
|
||||
);
|
||||
if (!mounted) return;
|
||||
}
|
||||
return _spawn();
|
||||
@@ -267,13 +270,9 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
// assigned by `--fork-session` and arrives in the init event (T-172).
|
||||
_sessionId ??= freshSessionId();
|
||||
try {
|
||||
managed = await orch.spawn(SpawnSpec(
|
||||
id: _orchId,
|
||||
role: 'fork ${widget.secondaryIndex}',
|
||||
sessionId: _sessionId!,
|
||||
cwd: repoRoot,
|
||||
forkSourceSessionId: forkSource,
|
||||
));
|
||||
managed = await orch.spawn(
|
||||
SpawnSpec(id: _orchId, role: 'fork ${widget.secondaryIndex}', sessionId: _sessionId!, cwd: repoRoot, forkSourceSessionId: forkSource),
|
||||
);
|
||||
} catch (e) {
|
||||
if (mounted) setState(() => _error = 'Could not start fork: $e');
|
||||
return;
|
||||
@@ -291,14 +290,16 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
final resume = await File(transcriptFile).exists();
|
||||
|
||||
try {
|
||||
managed = await orch.spawn(SpawnSpec(
|
||||
id: _orchId,
|
||||
role: widget.isPrimary ? 'primary' : 'session ${widget.secondaryIndex}',
|
||||
sessionId: _sessionId!,
|
||||
cwd: repoRoot,
|
||||
resume: resume,
|
||||
transcriptPath: resume ? transcriptFile : null,
|
||||
));
|
||||
managed = await orch.spawn(
|
||||
SpawnSpec(
|
||||
id: _orchId,
|
||||
role: widget.isPrimary ? 'primary' : 'session ${widget.secondaryIndex}',
|
||||
sessionId: _sessionId!,
|
||||
cwd: repoRoot,
|
||||
resume: resume,
|
||||
transcriptPath: resume ? transcriptFile : null,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
if (mounted) setState(() => _error = 'Could not start claude: $e');
|
||||
return;
|
||||
@@ -314,10 +315,10 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
// from the transcript/sidecar). Surfaces the resume path in `make run`.
|
||||
final seeded = _conversation?.items.length ?? 0;
|
||||
_kernel()?.log.info(
|
||||
'claude',
|
||||
'pane $_orchId bound session ${_sessionId ?? '?'} in $repoRoot — '
|
||||
'${seeded > 0 ? 'connected to history ($seeded seeded item(s))' : 'fresh session (no history)'}',
|
||||
);
|
||||
'claude',
|
||||
'pane $_orchId bound session ${_sessionId ?? '?'} in $repoRoot — '
|
||||
'${seeded > 0 ? 'connected to history ($seeded seeded item(s))' : 'fresh session (no history)'}',
|
||||
);
|
||||
_statusSub = managed.session.statusStream.listen((s) {
|
||||
if (!mounted) return;
|
||||
setState(() => _status = s);
|
||||
@@ -426,13 +427,7 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
final dir = Directory(claudeProjectDir(root));
|
||||
final sessions = await listSessions(dir);
|
||||
if (!mounted) return;
|
||||
final picked = await dialog.show<String>(
|
||||
(ctx, dismiss) => SessionPickerDialog(
|
||||
sessions: sessions,
|
||||
onPick: (id) => dismiss(id),
|
||||
onCancel: dismiss,
|
||||
),
|
||||
);
|
||||
final picked = await dialog.show<String>((ctx, dismiss) => SessionPickerDialog(sessions: sessions, onPick: (id) => dismiss(id), onCancel: dismiss));
|
||||
if (picked == null || !mounted) return;
|
||||
setState(() => _statusLine = 'resuming…');
|
||||
await _respawnWithSession(picked);
|
||||
@@ -479,10 +474,7 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
|
||||
final Widget body;
|
||||
if (_error != null) {
|
||||
body = Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ClideText(_error!, muted: true),
|
||||
);
|
||||
body = Padding(padding: const EdgeInsets.all(16), child: ClideText(_error!, muted: true));
|
||||
} else if (_conversation != null) {
|
||||
// Rebuild conversation + composer zone together on each prompt change so
|
||||
// the view hides a prompted tool-use card the moment its prompt appears
|
||||
@@ -520,7 +512,7 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
// with the conversation; renders nothing when there are no tasks.
|
||||
ListenableBuilder(
|
||||
listenable: _conversation!,
|
||||
builder: (_, __) => ClaudeTaskDock(tasks: taskListFrom(_conversation!.items)),
|
||||
builder: (_, _) => ClaudeTaskDock(tasks: taskListFrom(_conversation!.items)),
|
||||
),
|
||||
// An open prompt takes the composer's space and hides the text
|
||||
// input until it's answered, so interaction stays out of the
|
||||
@@ -557,22 +549,11 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
body = const Center(child: ClideText('starting…', muted: true));
|
||||
}
|
||||
|
||||
final content = widget.showChrome
|
||||
? ClidePaneChrome(
|
||||
title: title,
|
||||
subtitle: _error ?? _statusLine,
|
||||
child: body,
|
||||
)
|
||||
: body;
|
||||
final content = widget.showChrome ? ClidePaneChrome(title: title, subtitle: _error ?? _statusLine, child: body) : body;
|
||||
|
||||
// Surface this pane's status to the bottom status-bar slot while it's
|
||||
// the focused pane (T-150).
|
||||
return ClidePane(
|
||||
contributionId: widget.contributionId,
|
||||
active: widget.active,
|
||||
statusWidget: _statusWidget(tokens),
|
||||
child: content,
|
||||
);
|
||||
return ClidePane(contributionId: widget.contributionId, active: widget.active, statusWidget: _statusWidget(tokens), child: content);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,13 +571,7 @@ class _ModeBadge extends StatelessWidget {
|
||||
return Semantics(
|
||||
label: 'permission mode: ${permissionModeLabel(mode)}',
|
||||
excludeSemantics: true,
|
||||
child: ClideText(
|
||||
permissionModeLabel(mode),
|
||||
fontSize: clideFontSmall,
|
||||
fontFamily: clideMonoFamily,
|
||||
color: permissionModeColor(mode, tokens),
|
||||
maxLines: 1,
|
||||
),
|
||||
child: ClideText(permissionModeLabel(mode), fontSize: clideFontSmall, fontFamily: clideMonoFamily, color: permissionModeColor(mode, tokens), maxLines: 1),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,11 +85,13 @@ class ClaudeSessionHostState extends State<ClaudeSessionHost> {
|
||||
/// Public entry point used by the `claude.new-secondary` command.
|
||||
void addSecondary() {
|
||||
final index = _nextSecondary++;
|
||||
_controller.add(MultitabEntry<_Session>(
|
||||
id: 'secondary-$index',
|
||||
title: 'session $index',
|
||||
payload: _Session(isPrimary: false, secondaryIndex: index),
|
||||
));
|
||||
_controller.add(
|
||||
MultitabEntry<_Session>(
|
||||
id: 'secondary-$index',
|
||||
title: 'session $index',
|
||||
payload: _Session(isPrimary: false, secondaryIndex: index),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Open a new pane as a fork of [sourceClaudeSessionId] (T-172).
|
||||
@@ -99,11 +101,13 @@ class ClaudeSessionHostState extends State<ClaudeSessionHost> {
|
||||
/// without touching the original.
|
||||
void addFork(String sourceClaudeSessionId) {
|
||||
final index = _nextSecondary++;
|
||||
_controller.add(MultitabEntry<_Session>(
|
||||
id: 'secondary-$index',
|
||||
title: 'fork $index',
|
||||
payload: _Session(isPrimary: false, secondaryIndex: index, forkSourceId: sourceClaudeSessionId),
|
||||
));
|
||||
_controller.add(
|
||||
MultitabEntry<_Session>(
|
||||
id: 'secondary-$index',
|
||||
title: 'fork $index',
|
||||
payload: _Session(isPrimary: false, secondaryIndex: index, forkSourceId: sourceClaudeSessionId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -8,12 +8,7 @@ library;
|
||||
import 'dart:convert';
|
||||
|
||||
class DailyActivity {
|
||||
const DailyActivity({
|
||||
required this.date,
|
||||
required this.messageCount,
|
||||
required this.sessionCount,
|
||||
required this.toolCallCount,
|
||||
});
|
||||
const DailyActivity({required this.date, required this.messageCount, required this.sessionCount, required this.toolCallCount});
|
||||
|
||||
final String date; // "YYYY-MM-DD" (sorts chronologically as a string)
|
||||
final int messageCount;
|
||||
@@ -54,12 +49,14 @@ ClaudeStats parseClaudeStats(String jsonStr) {
|
||||
if (da is List) {
|
||||
for (final e in da) {
|
||||
if (e is! Map) continue;
|
||||
daily.add(DailyActivity(
|
||||
date: '${e['date']}',
|
||||
messageCount: _int(e['messageCount']),
|
||||
sessionCount: _int(e['sessionCount']),
|
||||
toolCallCount: _int(e['toolCallCount']),
|
||||
));
|
||||
daily.add(
|
||||
DailyActivity(
|
||||
date: '${e['date']}',
|
||||
messageCount: _int(e['messageCount']),
|
||||
sessionCount: _int(e['sessionCount']),
|
||||
toolCallCount: _int(e['toolCallCount']),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return ClaudeStats(lastComputed: j['lastComputedDate'] as String?, daily: daily);
|
||||
|
||||
@@ -62,10 +62,7 @@ String nextSafePermissionMode(String current) {
|
||||
if (s.cost != null) '\$${s.cost!.toStringAsFixed(2)}',
|
||||
if (s.rateLimitInfo != null) s.rateLimitInfo!,
|
||||
].join(' · ');
|
||||
return (
|
||||
leading: s.model != null ? shortModelLabel(s.model!) : null,
|
||||
trailing: trailing.isEmpty ? null : trailing,
|
||||
);
|
||||
return (leading: s.model != null ? shortModelLabel(s.model!) : null, trailing: trailing.isEmpty ? null : trailing);
|
||||
}
|
||||
|
||||
/// Friendly label for Claude's permission modes.
|
||||
|
||||
@@ -62,10 +62,7 @@ class _ClaudeTaskDockState extends State<ClaudeTaskDock> {
|
||||
ClideDivider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 4, 10, 6),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [for (final t in tasks) _taskRow(tokens, t)],
|
||||
),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [for (final t in tasks) _taskRow(tokens, t)]),
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -76,20 +73,22 @@ class _ClaudeTaskDockState extends State<ClaudeTaskDock> {
|
||||
}
|
||||
|
||||
Widget _summaryRow(SurfaceTokens tokens, String summary, String? current) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
ClideIcon(_expanded ? const ChevronDownIcon() : const ChevronRightIcon(), size: 12, color: tokens.globalTextMuted),
|
||||
const SizedBox(width: 8),
|
||||
ClideText(summary, fontSize: clideFontCaption, color: tokens.globalTextMuted),
|
||||
if (!_expanded && current != null) ...[
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: ClideText(current, fontSize: clideFontCaption, color: tokens.globalTextMuted, maxLines: 1, overflow: TextOverflow.ellipsis)),
|
||||
] else
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
);
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
ClideIcon(_expanded ? const ChevronDownIcon() : const ChevronRightIcon(), size: 12, color: tokens.globalTextMuted),
|
||||
const SizedBox(width: 8),
|
||||
ClideText(summary, fontSize: clideFontCaption, color: tokens.globalTextMuted),
|
||||
if (!_expanded && current != null) ...[
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: ClideText(current, fontSize: clideFontCaption, color: tokens.globalTextMuted, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
] else
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Widget _taskRow(SurfaceTokens tokens, TaskItem t) {
|
||||
final (String glyph, Color color, String word) = switch (t.status) {
|
||||
@@ -106,14 +105,13 @@ class _ClaudeTaskDockState extends State<ClaudeTaskDock> {
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(padding: const EdgeInsets.only(top: 1), child: ClideIcon(PhosphorIcons.byName(glyph), size: 13, color: color)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 1),
|
||||
child: ClideIcon(PhosphorIcons.byName(glyph), size: 13, color: color),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: ClideText(
|
||||
t.text,
|
||||
fontSize: clideFontCaption,
|
||||
color: t.status == TaskStatus.completed ? tokens.globalTextMuted : tokens.globalForeground,
|
||||
),
|
||||
child: ClideText(t.text, fontSize: clideFontCaption, color: t.status == TaskStatus.completed ? tokens.globalTextMuted : tokens.globalForeground),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -99,16 +99,10 @@ String pasteCacheDir() {
|
||||
/// written to [tempDir] (default [pasteCacheDir]) and attached by its
|
||||
/// path. Returns an empty list when the clipboard holds neither, so the
|
||||
/// composer pastes text instead.
|
||||
Future<List<ComposerAttachment>> resolveClipboardAttachment(
|
||||
ClipboardSource source, {
|
||||
Directory? tempDir,
|
||||
DateTime Function() now = DateTime.now,
|
||||
}) async {
|
||||
Future<List<ComposerAttachment>> resolveClipboardAttachment(ClipboardSource source, {Directory? tempDir, DateTime Function() now = DateTime.now}) async {
|
||||
final files = await source.readFiles();
|
||||
if (files.isNotEmpty) {
|
||||
return [
|
||||
for (final p in files) ComposerAttachment(path: p, isImage: _looksLikeImage(p)),
|
||||
];
|
||||
return [for (final p in files) ComposerAttachment(path: p, isImage: _looksLikeImage(p))];
|
||||
}
|
||||
|
||||
final image = await source.readImage();
|
||||
|
||||
@@ -183,20 +183,13 @@ class _ConversationCardState extends State<ConversationCard> {
|
||||
if (!_collapsed) ...[
|
||||
const SizedBox(height: 4),
|
||||
widget.body,
|
||||
for (final seg in widget.extraSegments) ...[
|
||||
_segmentLabel(tokens, seg.label),
|
||||
seg.child,
|
||||
],
|
||||
for (final seg in widget.extraSegments) ...[_segmentLabel(tokens, seg.label), seg.child],
|
||||
],
|
||||
],
|
||||
);
|
||||
return Padding(
|
||||
padding: widget.margin,
|
||||
child: MouseRegion(
|
||||
onEnter: (_) => setState(() => _hover = true),
|
||||
onExit: (_) => setState(() => _hover = false),
|
||||
child: _frame(tokens, content),
|
||||
),
|
||||
child: MouseRegion(onEnter: (_) => setState(() => _hover = true), onExit: (_) => setState(() => _hover = false), child: _frame(tokens, content)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -289,11 +282,7 @@ class _ConversationCardState extends State<ConversationCard> {
|
||||
onTap: () => setState(() => _collapsed = !_collapsed),
|
||||
builder: (_, hovered, pressed) => Padding(
|
||||
padding: const EdgeInsets.only(right: 6),
|
||||
child: ClideIcon(
|
||||
_collapsed ? PhosphorIcons.byName('caret-right') : PhosphorIcons.byName('caret-down'),
|
||||
size: 12,
|
||||
color: tokens.globalTextMuted,
|
||||
),
|
||||
child: ClideIcon(_collapsed ? PhosphorIcons.byName('caret-right') : PhosphorIcons.byName('caret-down'), size: 12, color: tokens.globalTextMuted),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -330,15 +319,15 @@ class _ConversationCardState extends State<ConversationCard> {
|
||||
/// A muted sub-label + hairline divider introducing an [CardSegment] below
|
||||
/// the primary body (T-262), so CALL/PROMPT/RESULT read as distinct parts.
|
||||
Widget _segmentLabel(SurfaceTokens tokens, String label) => Padding(
|
||||
padding: const EdgeInsets.only(top: 8, bottom: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
ClideText(label, fontSize: clideFontMeta, color: tokens.globalTextMuted, fontFamily: clideMonoFamily),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Container(height: 1, color: tokens.panelBorder)),
|
||||
],
|
||||
),
|
||||
);
|
||||
padding: const EdgeInsets.only(top: 8, bottom: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
ClideText(label, fontSize: clideFontMeta, color: tokens.globalTextMuted, fontFamily: clideMonoFamily),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Container(height: 1, color: tokens.panelBorder)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
List<Widget> _actions(SurfaceTokens tokens) {
|
||||
final items = <_ActionItem>[];
|
||||
@@ -360,12 +349,7 @@ class _ConversationCardState extends State<ConversationCard> {
|
||||
onTap: items[i].onTap,
|
||||
builder: (_, hovered, pressed) => Padding(
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
child: ClideText(
|
||||
items[i].label,
|
||||
fontSize: clideFontMeta,
|
||||
color: tokens.globalTextMuted,
|
||||
fontFamily: clideMonoFamily,
|
||||
),
|
||||
child: ClideText(items[i].label, fontSize: clideFontMeta, color: tokens.globalTextMuted, fontFamily: clideMonoFamily),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -21,11 +21,8 @@ class ConversationController extends ChangeNotifier {
|
||||
/// over stream-json so the pane would otherwise start empty. [onDispose]
|
||||
/// is invoked from [dispose] — wire it to the reader's `dispose` so
|
||||
/// cancelling the view tears down the underlying tail.
|
||||
ConversationController({
|
||||
required Stream<ConversationItem> stream,
|
||||
Iterable<ConversationItem>? seed,
|
||||
Future<void> Function()? onDispose,
|
||||
}) : _onDispose = onDispose {
|
||||
ConversationController({required Stream<ConversationItem> stream, Iterable<ConversationItem>? seed, Future<void> Function()? onDispose})
|
||||
: _onDispose = onDispose {
|
||||
if (seed != null) _items.addAll(seed);
|
||||
_sub = stream.listen(_onItem);
|
||||
}
|
||||
@@ -34,13 +31,10 @@ class ConversationController extends ChangeNotifier {
|
||||
/// the [ConversationItem]s a [TranscriptPublisher] writes onto
|
||||
/// [publisher]/[channel]. Decouples the view from the reader so several
|
||||
/// panels can render the same conversation (team work, T-139/T-140).
|
||||
factory ConversationController.fromBus({
|
||||
required MessageBus messages,
|
||||
String channel = ClaudeConversation.leadChannel,
|
||||
Future<void> Function()? onDispose,
|
||||
}) {
|
||||
final stream =
|
||||
messages.subscribe(publisher: ClaudeConversation.publisher, channel: channel).map((m) => m.data[ClaudeConversation.itemKey] as ConversationItem);
|
||||
factory ConversationController.fromBus({required MessageBus messages, String channel = ClaudeConversation.leadChannel, Future<void> Function()? onDispose}) {
|
||||
final stream = messages
|
||||
.subscribe(publisher: ClaudeConversation.publisher, channel: channel)
|
||||
.map((m) => m.data[ClaudeConversation.itemKey] as ConversationItem);
|
||||
return ConversationController(stream: stream, onDispose: onDispose);
|
||||
}
|
||||
|
||||
|
||||
@@ -181,11 +181,9 @@ class _ConversationViewState extends State<ConversationView> {
|
||||
/// - [runByToolUseId]: the rest of the run — prose / thinking / tool cards —
|
||||
/// nested in a holder UNDER the Agent card (T-264), with a successful
|
||||
/// sidechain tool result left out (it folds into its own tool card).
|
||||
({
|
||||
Set<String> ownedSidechainUuids,
|
||||
Map<String, List<UserMessage>> promptsByToolUseId,
|
||||
Map<String, List<ConversationItem>> runByToolUseId,
|
||||
}) _sidechainFold(List<ConversationItem> items) {
|
||||
({Set<String> ownedSidechainUuids, Map<String, List<UserMessage>> promptsByToolUseId, Map<String, List<ConversationItem>> runByToolUseId}) _sidechainFold(
|
||||
List<ConversationItem> items,
|
||||
) {
|
||||
final agentByMsgUuid = <String, AssistantToolUse>{
|
||||
for (final it in items)
|
||||
if (it is AssistantToolUse && _isAgentTool(it.name)) it.uuid: it,
|
||||
@@ -308,39 +306,39 @@ class _ConversationViewState extends State<ConversationView> {
|
||||
// visible list don't reattach State to the wrong card (T-285).
|
||||
return switch (g) {
|
||||
StickyItem(:final item) => _ConversationTurn(
|
||||
key: ValueKey('turn.${item.uuid}'),
|
||||
item: item,
|
||||
tokens: tokens,
|
||||
collapseTools: true,
|
||||
toolUseOutcomes: widget.toolUseOutcomes,
|
||||
quietErrorToolUseIds: widget.quietErrorToolUseIds,
|
||||
toolUseById: widget.controller.toolUseById,
|
||||
resultByToolUseId: resultByToolUseId,
|
||||
promptsByToolUseId: fold.promptsByToolUseId,
|
||||
runByToolUseId: fold.runByToolUseId,
|
||||
),
|
||||
key: ValueKey('turn.${item.uuid}'),
|
||||
item: item,
|
||||
tokens: tokens,
|
||||
collapseTools: true,
|
||||
toolUseOutcomes: widget.toolUseOutcomes,
|
||||
quietErrorToolUseIds: widget.quietErrorToolUseIds,
|
||||
toolUseById: widget.controller.toolUseById,
|
||||
resultByToolUseId: resultByToolUseId,
|
||||
promptsByToolUseId: fold.promptsByToolUseId,
|
||||
runByToolUseId: fold.runByToolUseId,
|
||||
),
|
||||
FoldedCluster(:final items) => _ActivityCard(
|
||||
key: ValueKey('cluster.${items.first.uuid}'),
|
||||
items: items,
|
||||
tokens: tokens,
|
||||
toolUseOutcomes: widget.toolUseOutcomes,
|
||||
quietErrorToolUseIds: widget.quietErrorToolUseIds,
|
||||
toolUseById: widget.controller.toolUseById,
|
||||
resultByToolUseId: resultByToolUseId,
|
||||
promptsByToolUseId: fold.promptsByToolUseId,
|
||||
runByToolUseId: fold.runByToolUseId,
|
||||
),
|
||||
key: ValueKey('cluster.${items.first.uuid}'),
|
||||
items: items,
|
||||
tokens: tokens,
|
||||
toolUseOutcomes: widget.toolUseOutcomes,
|
||||
quietErrorToolUseIds: widget.quietErrorToolUseIds,
|
||||
toolUseById: widget.controller.toolUseById,
|
||||
resultByToolUseId: resultByToolUseId,
|
||||
promptsByToolUseId: fold.promptsByToolUseId,
|
||||
runByToolUseId: fold.runByToolUseId,
|
||||
),
|
||||
EditRun(:final edits) => _EditRunCard(
|
||||
key: ValueKey('edits.${edits.first.uuid}'),
|
||||
edits: edits,
|
||||
tokens: tokens,
|
||||
toolUseOutcomes: widget.toolUseOutcomes,
|
||||
quietErrorToolUseIds: widget.quietErrorToolUseIds,
|
||||
toolUseById: widget.controller.toolUseById,
|
||||
resultByToolUseId: resultByToolUseId,
|
||||
promptsByToolUseId: fold.promptsByToolUseId,
|
||||
runByToolUseId: fold.runByToolUseId,
|
||||
),
|
||||
key: ValueKey('edits.${edits.first.uuid}'),
|
||||
edits: edits,
|
||||
tokens: tokens,
|
||||
toolUseOutcomes: widget.toolUseOutcomes,
|
||||
quietErrorToolUseIds: widget.quietErrorToolUseIds,
|
||||
toolUseById: widget.controller.toolUseById,
|
||||
resultByToolUseId: resultByToolUseId,
|
||||
promptsByToolUseId: fold.promptsByToolUseId,
|
||||
runByToolUseId: fold.runByToolUseId,
|
||||
),
|
||||
};
|
||||
},
|
||||
),
|
||||
@@ -413,7 +411,7 @@ String? resolveWorkspaceFilePath(String? root, String raw) {
|
||||
/// Open a clicked workspace file reference in the editor, jumping to [line]
|
||||
/// when present — the Dart-side twin of `clide editor open <path>` (T-300, D-6).
|
||||
void _openFile(BuildContext context, String path, int? line) {
|
||||
unawaited(ClideKernel.of(context).ipc.request('editor.open', args: {'path': path, if (line != null) 'line': line}));
|
||||
unawaited(ClideKernel.of(context).ipc.request('editor.open', args: {'path': path, 'line': ?line}));
|
||||
}
|
||||
|
||||
/// One conversation item, rendered by kind.
|
||||
@@ -474,63 +472,63 @@ class _ConversationTurn extends StatelessWidget {
|
||||
// sidechain prompt here is an orphan one (its Agent card couldn't be
|
||||
// resolved) — folded prompts are suppressed upstream (T-263).
|
||||
UserMessage() when i.injected || i.isSidechain => ConversationCard(
|
||||
// Framed like every other card (T-306) — just muted + collapsed, not
|
||||
// the blue "you" accent (D-78); bare read as unfinished next to the
|
||||
// carded tool calls.
|
||||
variant: ConversationCardVariant.bordered,
|
||||
accent: tokens.globalTextMuted,
|
||||
label: i.isSidechain ? 'agent prompt' : 'context',
|
||||
copyText: i.text,
|
||||
collapsible: true,
|
||||
collapsedByDefault: true,
|
||||
collapsedSummary: _firstLine(i.text),
|
||||
margin: _childMargin,
|
||||
body: ClideText(i.text, muted: true, fontSize: clideFontMeta),
|
||||
),
|
||||
// Framed like every other card (T-306) — just muted + collapsed, not
|
||||
// the blue "you" accent (D-78); bare read as unfinished next to the
|
||||
// carded tool calls.
|
||||
variant: ConversationCardVariant.bordered,
|
||||
accent: tokens.globalTextMuted,
|
||||
label: i.isSidechain ? 'agent prompt' : 'context',
|
||||
copyText: i.text,
|
||||
collapsible: true,
|
||||
collapsedByDefault: true,
|
||||
collapsedSummary: _firstLine(i.text),
|
||||
margin: _childMargin,
|
||||
body: ClideText(i.text, muted: true, fontSize: clideFontMeta),
|
||||
),
|
||||
UserMessage() => ConversationCard(
|
||||
accent: tokens.globalFocus,
|
||||
label: 'you',
|
||||
copyText: i.text,
|
||||
margin: _childMargin,
|
||||
// Pasted-image @path tokens render as inline thumbnails that open the
|
||||
// lightbox (T-236/T-254); copyText keeps the original text verbatim.
|
||||
body: ClideMarkdown(
|
||||
i.text,
|
||||
onRecordTap: (id) => _openRecord(context, id),
|
||||
onImageToken: (path) => ImageThumbnail(path: path, size: 48),
|
||||
onLinkTap: (url) => _openUrl(context, url),
|
||||
resolveFileRef: (p) => _resolveRepoFile(context, p),
|
||||
onOpenFile: (path, line) => _openFile(context, path, line),
|
||||
),
|
||||
accent: tokens.globalFocus,
|
||||
label: 'you',
|
||||
copyText: i.text,
|
||||
margin: _childMargin,
|
||||
// Pasted-image @path tokens render as inline thumbnails that open the
|
||||
// lightbox (T-236/T-254); copyText keeps the original text verbatim.
|
||||
body: ClideMarkdown(
|
||||
i.text,
|
||||
onRecordTap: (id) => _openRecord(context, id),
|
||||
onImageToken: (path) => ImageThumbnail(path: path, size: 48),
|
||||
onLinkTap: (url) => _openUrl(context, url),
|
||||
resolveFileRef: (p) => _resolveRepoFile(context, p),
|
||||
onOpenFile: (path, line) => _openFile(context, path, line),
|
||||
),
|
||||
),
|
||||
// Sub-agent (sidechain) prose is NOT the main Claude — attribute it to the
|
||||
// agent with a muted accent, never the coral "claude" brand (T-265). The
|
||||
// coral claudeAccent is reserved for the real main-thread Claude.
|
||||
AssistantTextMessage() => ConversationCard(
|
||||
accent: i.isSidechain ? tokens.globalTextMuted : claudeAccent,
|
||||
label: i.isSidechain ? 'agent' : 'claude',
|
||||
copyText: i.text,
|
||||
margin: _childMargin,
|
||||
body: ClideMarkdown(
|
||||
i.text,
|
||||
onRecordTap: (id) => _openRecord(context, id),
|
||||
onLinkTap: (url) => _openUrl(context, url),
|
||||
resolveFileRef: (p) => _resolveRepoFile(context, p),
|
||||
onOpenFile: (path, line) => _openFile(context, path, line),
|
||||
),
|
||||
accent: i.isSidechain ? tokens.globalTextMuted : claudeAccent,
|
||||
label: i.isSidechain ? 'agent' : 'claude',
|
||||
copyText: i.text,
|
||||
margin: _childMargin,
|
||||
body: ClideMarkdown(
|
||||
i.text,
|
||||
onRecordTap: (id) => _openRecord(context, id),
|
||||
onLinkTap: (url) => _openUrl(context, url),
|
||||
resolveFileRef: (p) => _resolveRepoFile(context, p),
|
||||
onOpenFile: (path, line) => _openFile(context, path, line),
|
||||
),
|
||||
),
|
||||
AssistantThinkingMessage() => ConversationCard(
|
||||
// Framed + muted like the context card (T-306).
|
||||
variant: ConversationCardVariant.bordered,
|
||||
accent: tokens.globalTextMuted,
|
||||
label: i.isSidechain ? 'agent thinking' : 'thinking',
|
||||
copyText: i.thinking,
|
||||
collapsible: true,
|
||||
collapsedByDefault: true,
|
||||
collapsedSummary: _firstLine(i.thinking),
|
||||
margin: _childMargin,
|
||||
body: ClideText(i.thinking, muted: true, fontSize: clideFontMeta),
|
||||
),
|
||||
// Framed + muted like the context card (T-306).
|
||||
variant: ConversationCardVariant.bordered,
|
||||
accent: tokens.globalTextMuted,
|
||||
label: i.isSidechain ? 'agent thinking' : 'thinking',
|
||||
copyText: i.thinking,
|
||||
collapsible: true,
|
||||
collapsedByDefault: true,
|
||||
collapsedSummary: _firstLine(i.thinking),
|
||||
margin: _childMargin,
|
||||
body: ClideText(i.thinking, muted: true, fontSize: clideFontMeta),
|
||||
),
|
||||
AssistantToolUse() => collapseTools ? _toolUseCollapser(i) : _toolContentCard(i),
|
||||
ToolResultMessage() => _toolResult(i),
|
||||
ImageMessage() => _image(context, i),
|
||||
@@ -566,16 +564,13 @@ class _ConversationTurn extends StatelessWidget {
|
||||
image: ClideFileImage(m.path),
|
||||
fit: BoxFit.contain,
|
||||
alignment: Alignment.centerLeft,
|
||||
errorBuilder: (_, __, ___) => _imagePlaceholder(m.path),
|
||||
errorBuilder: (_, _, _) => _imagePlaceholder(m.path),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (caption != null && caption.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
ClideText(caption, fontSize: clideFontMeta, color: tokens.globalTextMuted),
|
||||
],
|
||||
if (caption != null && caption.isNotEmpty) ...[const SizedBox(height: 4), ClideText(caption, fontSize: clideFontMeta, color: tokens.globalTextMuted)],
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -583,34 +578,30 @@ class _ConversationTurn extends StatelessWidget {
|
||||
|
||||
void _openLightbox(BuildContext context, String path) {
|
||||
ClideKernel.of(context).dialog.show<Object>(
|
||||
(ctx, dismiss) => ClideLightbox(
|
||||
onDismiss: dismiss,
|
||||
child: Image(
|
||||
image: ClideFileImage(path),
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, __, ___) => _imagePlaceholder(path),
|
||||
),
|
||||
),
|
||||
);
|
||||
(ctx, dismiss) => ClideLightbox(
|
||||
onDismiss: dismiss,
|
||||
child: Image(image: ClideFileImage(path), fit: BoxFit.contain, errorBuilder: (_, _, _) => _imagePlaceholder(path)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _imagePlaceholder(String path) => Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: tokens.panelBorder),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: tokens.panelBorder),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
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: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
/// A standalone tool use (T-305): every tool use is a collapser over a
|
||||
/// one-item list. The collapser carries the echoed last line, the count, and
|
||||
@@ -685,8 +676,15 @@ class _ConversationTurn extends StatelessWidget {
|
||||
// (note E): call input (body) → prompt → returned result.
|
||||
final segments = <CardSegment>[
|
||||
for (final p in promptsByToolUseId[t.toolUseId] ?? const <UserMessage>[])
|
||||
CardSegment(label: 'prompt', child: ClideText(p.text, muted: true, fontSize: clideFontMeta)),
|
||||
if (succeeded && !(isAgent && hasRun)) CardSegment(label: 'result', child: ClideCodeBlock(source: result.content, language: _resultLanguage(t))),
|
||||
CardSegment(
|
||||
label: 'prompt',
|
||||
child: ClideText(p.text, muted: true, fontSize: clideFontMeta),
|
||||
),
|
||||
if (succeeded && !(isAgent && hasRun))
|
||||
CardSegment(
|
||||
label: 'result',
|
||||
child: ClideCodeBlock(source: result.content, language: _resultLanguage(t)),
|
||||
),
|
||||
];
|
||||
|
||||
// A resolved permission-prompted call is tinted green if approved / red if
|
||||
@@ -757,12 +755,7 @@ class _ConversationTurn extends StatelessWidget {
|
||||
collapsible: quiet || multiline,
|
||||
collapsedByDefault: quiet, // genuine errors stay expanded; a denial folds
|
||||
collapsedSummary: (quiet || multiline) ? _firstLine(t.content) : null,
|
||||
body: ClideText(
|
||||
t.content,
|
||||
fontSize: clideFontMeta,
|
||||
fontFamily: clideMonoFamily,
|
||||
color: quiet ? tokens.globalTextMuted : tokens.statusError,
|
||||
),
|
||||
body: ClideText(t.content, fontSize: clideFontMeta, fontFamily: clideMonoFamily, color: quiet ? tokens.globalTextMuted : tokens.statusError),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -783,12 +776,7 @@ class _ConversationTurn extends StatelessWidget {
|
||||
collapsedSummary: multiline ? _firstLine(t.content) : null,
|
||||
body: isOutputTool
|
||||
? ClideCodeBlock(source: t.content, language: 'text')
|
||||
: ClideText(
|
||||
t.content,
|
||||
fontSize: clideFontMeta,
|
||||
fontFamily: clideMonoFamily,
|
||||
color: tokens.globalForeground,
|
||||
),
|
||||
: ClideText(t.content, fontSize: clideFontMeta, fontFamily: clideMonoFamily, color: tokens.globalForeground),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,270 +48,259 @@ class ClaudeExtension extends ClideExtension {
|
||||
|
||||
@override
|
||||
List<ContributionPoint> get contributions => [
|
||||
TabContribution(
|
||||
id: 'claude.primary',
|
||||
slot: Slots.workspace,
|
||||
title: 'Claude',
|
||||
titleKey: 'tab.title',
|
||||
i18nNamespace: id,
|
||||
priority: 90,
|
||||
build: (_) => TeamPanelHost(lead: ClaudeSessionHost(key: _hostKey)),
|
||||
),
|
||||
CommandContribution(
|
||||
id: 'claude.new-secondary',
|
||||
command: 'claude.new-secondary',
|
||||
title: 'Claude: open a secondary session',
|
||||
run: (_) async {
|
||||
_hostKey.currentState?.addSecondary();
|
||||
return IpcResponse.ok(id: '', data: const {'status': 'spawned'});
|
||||
},
|
||||
),
|
||||
CommandContribution(
|
||||
id: 'claude.kill-all-sessions',
|
||||
command: 'claude.kill-all-sessions',
|
||||
title: 'Claude: kill all sessions for this repo',
|
||||
run: _killAllSessions,
|
||||
),
|
||||
CommandContribution(
|
||||
id: 'claude.session-storage',
|
||||
command: 'claude.session-storage',
|
||||
title: 'Claude: session storage (disk usage + cleanup)',
|
||||
run: _manageStorage,
|
||||
),
|
||||
// T-235: cycle how aggressively the activity card folds meta steps
|
||||
// (none → tools → thinking → everything), persisted app-wide. The panes
|
||||
// read kActivityFoldLevelKey and rebuild via the settings notifier.
|
||||
CommandContribution(
|
||||
id: 'claude.activity.fold-level',
|
||||
command: 'claude.activity.fold-level',
|
||||
title: 'Claude: cycle activity fold level',
|
||||
run: _cycleFoldLevel,
|
||||
),
|
||||
// T-171: agent roster controls (D-6 CLI/UI parity).
|
||||
// Usage: clide claude.agent.show <sessionId>
|
||||
CommandContribution(
|
||||
id: 'claude.agent.show',
|
||||
command: 'claude.agent.show',
|
||||
title: 'Claude: show an agent session pane',
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return IpcResponse.ok(id: '', data: const {'error': 'missing session id'});
|
||||
_orchestrator?.show(id);
|
||||
return IpcResponse.ok(id: '', data: {'id': id, 'status': 'shown'});
|
||||
},
|
||||
),
|
||||
CommandContribution(
|
||||
id: 'claude.agent.hide',
|
||||
command: 'claude.agent.hide',
|
||||
title: 'Claude: hide an agent session pane',
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return IpcResponse.ok(id: '', data: const {'error': 'missing session id'});
|
||||
_orchestrator?.hide(id);
|
||||
return IpcResponse.ok(id: '', data: {'id': id, 'status': 'hidden'});
|
||||
},
|
||||
),
|
||||
CommandContribution(
|
||||
id: 'claude.agent.close',
|
||||
command: 'claude.agent.close',
|
||||
title: 'Claude: close (kill) an agent session',
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return IpcResponse.ok(id: '', data: const {'error': 'missing session id'});
|
||||
await _orchestrator?.close(id);
|
||||
return IpcResponse.ok(id: '', data: {'id': id, 'status': 'closed'});
|
||||
},
|
||||
),
|
||||
CommandContribution(
|
||||
id: 'claude.agent.mute',
|
||||
command: 'claude.agent.mute',
|
||||
title: 'Claude: mute broker delivery to an agent session',
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return IpcResponse.ok(id: '', data: const {'error': 'missing session id'});
|
||||
_orchestrator?.mute(id);
|
||||
return IpcResponse.ok(id: '', data: {'id': id, 'status': 'muted'});
|
||||
},
|
||||
),
|
||||
CommandContribution(
|
||||
id: 'claude.agent.unmute',
|
||||
command: 'claude.agent.unmute',
|
||||
title: 'Claude: unmute broker delivery to an agent session',
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return IpcResponse.ok(id: '', data: const {'error': 'missing session id'});
|
||||
_orchestrator?.unmute(id);
|
||||
return IpcResponse.ok(id: '', data: {'id': id, 'status': 'unmuted'});
|
||||
},
|
||||
),
|
||||
// Usage: clide claude.agent.inject-message <sessionId> <text...>
|
||||
CommandContribution(
|
||||
id: 'claude.agent.inject-message',
|
||||
command: 'claude.agent.inject-message',
|
||||
title: 'Claude: inject a text turn into an agent session',
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return IpcResponse.ok(id: '', data: const {'error': 'missing session id'});
|
||||
final text = args.skip(1).join(' ');
|
||||
if (text.isEmpty) return IpcResponse.ok(id: '', data: const {'error': 'missing message text'});
|
||||
_orchestrator?.injectMessage(id, text);
|
||||
return IpcResponse.ok(id: '', data: {'id': id, 'status': 'injected'});
|
||||
},
|
||||
),
|
||||
// T-181: set permission mode for an agent session (D-6 CLI/UI parity).
|
||||
// Usage: clide claude.agent.set-permission-mode <sessionId> <mode>
|
||||
// <mode> must be one of: default, acceptEdits, plan, bypassPermissions.
|
||||
// Note: bypassPermissions is accepted via CLI — the footgun guard is the
|
||||
// UI's confirm dialog; the CLI caller is responsible for their own safety.
|
||||
CommandContribution(
|
||||
id: 'claude.agent.set-permission-mode',
|
||||
command: 'claude.agent.set-permission-mode',
|
||||
title: 'Claude: set permission mode for an agent session',
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return IpcResponse.ok(id: '', data: const {'error': 'missing session id'});
|
||||
final mode = args.length >= 2 ? args[1] : null;
|
||||
if (mode == null) return IpcResponse.ok(id: '', data: const {'error': 'missing mode (default|acceptEdits|plan|bypassPermissions)'});
|
||||
const valid = {'default', 'acceptEdits', 'plan', 'bypassPermissions'};
|
||||
if (!valid.contains(mode)) {
|
||||
return IpcResponse.ok(id: '', data: {'error': 'unknown mode "$mode"; use one of: ${valid.join(', ')}'});
|
||||
}
|
||||
_orchestrator?.byId(id)?.session.setPermissionMode(mode);
|
||||
return IpcResponse.ok(id: '', data: {'id': id, 'mode': mode, 'status': 'sent'});
|
||||
},
|
||||
),
|
||||
// T-226: cycle the primary session's permission mode through the safe
|
||||
// trio. Palette-discoverable counterpart to the composer's Ctrl/Cmd+M.
|
||||
CommandContribution(
|
||||
id: 'claude.mode.cycle',
|
||||
command: 'claude.mode.cycle',
|
||||
title: 'Claude: Cycle permission mode',
|
||||
run: (_) async {
|
||||
final managed = _orchestrator?.byId('primary');
|
||||
if (managed == null) return IpcResponse.ok(id: '', data: const {'error': 'no primary session'});
|
||||
final next = nextSafePermissionMode(managed.session.status.permissionMode ?? 'default');
|
||||
managed.session.setPermissionMode(next);
|
||||
return IpcResponse.ok(id: '', data: {'mode': next, 'status': 'sent'});
|
||||
},
|
||||
),
|
||||
// Usage: clide claude.task.reassign <taskId> <toSessionId>
|
||||
CommandContribution(
|
||||
id: 'claude.task.reassign',
|
||||
command: 'claude.task.reassign',
|
||||
title: 'Claude: reassign a shared task to an agent',
|
||||
run: (args) async {
|
||||
if (args.length < 2) return IpcResponse.ok(id: '', data: const {'error': 'usage: <taskId> <sessionId>'});
|
||||
final taskId = args[0];
|
||||
final toId = args[1];
|
||||
final ok = _orchestrator?.broker.reassignTask(taskId, toId) ?? false;
|
||||
return IpcResponse.ok(id: '', data: {'taskId': taskId, 'toId': toId, 'ok': ok});
|
||||
},
|
||||
),
|
||||
// T-180: full team chat pane opened as a workspace tab.
|
||||
// Shares the TeamChatModel with the sidebar widget.
|
||||
TabContribution(
|
||||
id: 'claude.team-chat',
|
||||
slot: Slots.workspace,
|
||||
title: 'Team Chat',
|
||||
titleKey: 'tab.title',
|
||||
i18nNamespace: id,
|
||||
priority: 85,
|
||||
build: (_) {
|
||||
final orch = _orchestrator;
|
||||
if (orch == null) return const SizedBox.shrink();
|
||||
return TeamChatPane(model: orch.chatModel, broker: orch.broker);
|
||||
},
|
||||
),
|
||||
// CLI parity: open the team chat pane from the shell.
|
||||
// Usage: clide claude.team-chat.open
|
||||
CommandContribution(
|
||||
id: 'claude.team-chat.open',
|
||||
command: 'claude.team-chat.open',
|
||||
title: 'Claude: open the team chat pane',
|
||||
run: (args) async {
|
||||
_ctx?.panels.activateTab(Slots.workspace, 'claude.team-chat');
|
||||
return IpcResponse.ok(id: '', data: const {'status': 'opened'});
|
||||
},
|
||||
),
|
||||
// Usage: clide claude.team-chat.post [@name] <text...>
|
||||
// Posts a message into the broker channel as the user.
|
||||
// Leading @name tag selects the recipient; omit for broadcast.
|
||||
CommandContribution(
|
||||
id: 'claude.team-chat.post',
|
||||
command: 'claude.team-chat.post',
|
||||
title: 'Claude: post a message into the team channel as the user',
|
||||
run: (args) async {
|
||||
if (args.isEmpty) return IpcResponse.ok(id: '', data: const {'error': 'usage: [@name] <text>'});
|
||||
final raw = args.join(' ');
|
||||
String? recipient;
|
||||
String body = raw;
|
||||
if (raw.startsWith('@')) {
|
||||
final ws = raw.indexOf(RegExp(r'\s'));
|
||||
if (ws > 0) {
|
||||
final tag = raw.substring(1, ws);
|
||||
recipient = (tag == 'team' || tag.isEmpty) ? null : tag;
|
||||
body = raw.substring(ws).trim();
|
||||
}
|
||||
}
|
||||
_orchestrator?.chatModel.postAsUser(body, toName: recipient);
|
||||
return IpcResponse.ok(id: '', data: {'status': 'posted', if (recipient != null) 'to': recipient});
|
||||
},
|
||||
),
|
||||
// claude.agent.fork: branch a managed session into a new fork session
|
||||
// (T-172, D-6 CLI/UI parity for the roster fork button).
|
||||
// Usage: clide claude.agent.fork <sourceSessionId> [<cwd>]
|
||||
// <sourceSessionId>: the clide-internal id of the session to fork.
|
||||
// <cwd>: optional working directory; defaults to the source session's cwd.
|
||||
CommandContribution(
|
||||
id: 'claude.agent.fork',
|
||||
command: 'claude.agent.fork',
|
||||
title: 'Claude: fork a managed session into a new branch session',
|
||||
run: (args) async {
|
||||
final sourceId = args.firstOrNull;
|
||||
if (sourceId == null) {
|
||||
return IpcResponse.ok(id: '', data: const {'error': 'usage: claude.agent.fork <sourceSessionId> [<cwd>]'});
|
||||
}
|
||||
final orch = _orchestrator;
|
||||
if (orch == null) {
|
||||
return IpcResponse.ok(id: '', data: const {'error': 'orchestrator unavailable'});
|
||||
}
|
||||
final source = orch.byId(sourceId);
|
||||
if (source == null) {
|
||||
return IpcResponse.ok(id: '', data: {'error': 'unknown session "$sourceId"'});
|
||||
}
|
||||
final cwd = args.length >= 2 ? args[1] : source.cwd;
|
||||
final forkId = 'fork:$sourceId-${DateTime.now().millisecondsSinceEpoch}';
|
||||
await orch.spawn(SpawnSpec(
|
||||
id: forkId,
|
||||
role: 'fork of $sourceId',
|
||||
sessionId: forkId,
|
||||
cwd: cwd,
|
||||
forkSourceSessionId: source.sessionId,
|
||||
));
|
||||
return IpcResponse.ok(id: '', data: {'forkId': forkId, 'sourceId': sourceId, 'status': 'spawned'});
|
||||
},
|
||||
),
|
||||
// Always-pickable left-panel tab: Claude activity (from
|
||||
// stats-cache.json) + the team roster when a team is running (T-141).
|
||||
TabContribution(
|
||||
id: 'claude.meta',
|
||||
slot: Slots.sidebar,
|
||||
title: 'Activity',
|
||||
icon: PhosphorIcons.byName('robot'),
|
||||
priority: 60,
|
||||
build: (_) => const ClaudeMetaSidebar(),
|
||||
),
|
||||
// In-pane status slot (T-145): the active Claude pane publishes
|
||||
// its model · permission-mode · context line here.
|
||||
// flex: 1 → StatusbarHost wraps this in Flexible(loose) so the slot
|
||||
// yields width under pressure and ClideMarquee scrolls (T-160).
|
||||
StatusItemContribution(
|
||||
id: 'claude.status-context',
|
||||
priority: 50,
|
||||
flex: 1,
|
||||
build: (_) => const PaneContextStatusItem(),
|
||||
),
|
||||
];
|
||||
TabContribution(
|
||||
id: 'claude.primary',
|
||||
slot: Slots.workspace,
|
||||
title: 'Claude',
|
||||
titleKey: 'tab.title',
|
||||
i18nNamespace: id,
|
||||
priority: 90,
|
||||
build: (_) => TeamPanelHost(lead: ClaudeSessionHost(key: _hostKey)),
|
||||
),
|
||||
CommandContribution(
|
||||
id: 'claude.new-secondary',
|
||||
command: 'claude.new-secondary',
|
||||
title: 'Claude: open a secondary session',
|
||||
run: (_) async {
|
||||
_hostKey.currentState?.addSecondary();
|
||||
return IpcResponse.ok(id: '', data: const {'status': 'spawned'});
|
||||
},
|
||||
),
|
||||
CommandContribution(
|
||||
id: 'claude.kill-all-sessions',
|
||||
command: 'claude.kill-all-sessions',
|
||||
title: 'Claude: kill all sessions for this repo',
|
||||
run: _killAllSessions,
|
||||
),
|
||||
CommandContribution(
|
||||
id: 'claude.session-storage',
|
||||
command: 'claude.session-storage',
|
||||
title: 'Claude: session storage (disk usage + cleanup)',
|
||||
run: _manageStorage,
|
||||
),
|
||||
// T-235: cycle how aggressively the activity card folds meta steps
|
||||
// (none → tools → thinking → everything), persisted app-wide. The panes
|
||||
// read kActivityFoldLevelKey and rebuild via the settings notifier.
|
||||
CommandContribution(
|
||||
id: 'claude.activity.fold-level',
|
||||
command: 'claude.activity.fold-level',
|
||||
title: 'Claude: cycle activity fold level',
|
||||
run: _cycleFoldLevel,
|
||||
),
|
||||
// T-171: agent roster controls (D-6 CLI/UI parity).
|
||||
// Usage: clide claude.agent.show <sessionId>
|
||||
CommandContribution(
|
||||
id: 'claude.agent.show',
|
||||
command: 'claude.agent.show',
|
||||
title: 'Claude: show an agent session pane',
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return IpcResponse.ok(id: '', data: const {'error': 'missing session id'});
|
||||
_orchestrator?.show(id);
|
||||
return IpcResponse.ok(id: '', data: {'id': id, 'status': 'shown'});
|
||||
},
|
||||
),
|
||||
CommandContribution(
|
||||
id: 'claude.agent.hide',
|
||||
command: 'claude.agent.hide',
|
||||
title: 'Claude: hide an agent session pane',
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return IpcResponse.ok(id: '', data: const {'error': 'missing session id'});
|
||||
_orchestrator?.hide(id);
|
||||
return IpcResponse.ok(id: '', data: {'id': id, 'status': 'hidden'});
|
||||
},
|
||||
),
|
||||
CommandContribution(
|
||||
id: 'claude.agent.close',
|
||||
command: 'claude.agent.close',
|
||||
title: 'Claude: close (kill) an agent session',
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return IpcResponse.ok(id: '', data: const {'error': 'missing session id'});
|
||||
await _orchestrator?.close(id);
|
||||
return IpcResponse.ok(id: '', data: {'id': id, 'status': 'closed'});
|
||||
},
|
||||
),
|
||||
CommandContribution(
|
||||
id: 'claude.agent.mute',
|
||||
command: 'claude.agent.mute',
|
||||
title: 'Claude: mute broker delivery to an agent session',
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return IpcResponse.ok(id: '', data: const {'error': 'missing session id'});
|
||||
_orchestrator?.mute(id);
|
||||
return IpcResponse.ok(id: '', data: {'id': id, 'status': 'muted'});
|
||||
},
|
||||
),
|
||||
CommandContribution(
|
||||
id: 'claude.agent.unmute',
|
||||
command: 'claude.agent.unmute',
|
||||
title: 'Claude: unmute broker delivery to an agent session',
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return IpcResponse.ok(id: '', data: const {'error': 'missing session id'});
|
||||
_orchestrator?.unmute(id);
|
||||
return IpcResponse.ok(id: '', data: {'id': id, 'status': 'unmuted'});
|
||||
},
|
||||
),
|
||||
// Usage: clide claude.agent.inject-message <sessionId> <text...>
|
||||
CommandContribution(
|
||||
id: 'claude.agent.inject-message',
|
||||
command: 'claude.agent.inject-message',
|
||||
title: 'Claude: inject a text turn into an agent session',
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return IpcResponse.ok(id: '', data: const {'error': 'missing session id'});
|
||||
final text = args.skip(1).join(' ');
|
||||
if (text.isEmpty) return IpcResponse.ok(id: '', data: const {'error': 'missing message text'});
|
||||
_orchestrator?.injectMessage(id, text);
|
||||
return IpcResponse.ok(id: '', data: {'id': id, 'status': 'injected'});
|
||||
},
|
||||
),
|
||||
// T-181: set permission mode for an agent session (D-6 CLI/UI parity).
|
||||
// Usage: clide claude.agent.set-permission-mode <sessionId> <mode>
|
||||
// <mode> must be one of: default, acceptEdits, plan, bypassPermissions.
|
||||
// Note: bypassPermissions is accepted via CLI — the footgun guard is the
|
||||
// UI's confirm dialog; the CLI caller is responsible for their own safety.
|
||||
CommandContribution(
|
||||
id: 'claude.agent.set-permission-mode',
|
||||
command: 'claude.agent.set-permission-mode',
|
||||
title: 'Claude: set permission mode for an agent session',
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return IpcResponse.ok(id: '', data: const {'error': 'missing session id'});
|
||||
final mode = args.length >= 2 ? args[1] : null;
|
||||
if (mode == null) return IpcResponse.ok(id: '', data: const {'error': 'missing mode (default|acceptEdits|plan|bypassPermissions)'});
|
||||
const valid = {'default', 'acceptEdits', 'plan', 'bypassPermissions'};
|
||||
if (!valid.contains(mode)) {
|
||||
return IpcResponse.ok(id: '', data: {'error': 'unknown mode "$mode"; use one of: ${valid.join(', ')}'});
|
||||
}
|
||||
_orchestrator?.byId(id)?.session.setPermissionMode(mode);
|
||||
return IpcResponse.ok(id: '', data: {'id': id, 'mode': mode, 'status': 'sent'});
|
||||
},
|
||||
),
|
||||
// T-226: cycle the primary session's permission mode through the safe
|
||||
// trio. Palette-discoverable counterpart to the composer's Ctrl/Cmd+M.
|
||||
CommandContribution(
|
||||
id: 'claude.mode.cycle',
|
||||
command: 'claude.mode.cycle',
|
||||
title: 'Claude: Cycle permission mode',
|
||||
run: (_) async {
|
||||
final managed = _orchestrator?.byId('primary');
|
||||
if (managed == null) return IpcResponse.ok(id: '', data: const {'error': 'no primary session'});
|
||||
final next = nextSafePermissionMode(managed.session.status.permissionMode ?? 'default');
|
||||
managed.session.setPermissionMode(next);
|
||||
return IpcResponse.ok(id: '', data: {'mode': next, 'status': 'sent'});
|
||||
},
|
||||
),
|
||||
// Usage: clide claude.task.reassign <taskId> <toSessionId>
|
||||
CommandContribution(
|
||||
id: 'claude.task.reassign',
|
||||
command: 'claude.task.reassign',
|
||||
title: 'Claude: reassign a shared task to an agent',
|
||||
run: (args) async {
|
||||
if (args.length < 2) return IpcResponse.ok(id: '', data: const {'error': 'usage: <taskId> <sessionId>'});
|
||||
final taskId = args[0];
|
||||
final toId = args[1];
|
||||
final ok = _orchestrator?.broker.reassignTask(taskId, toId) ?? false;
|
||||
return IpcResponse.ok(id: '', data: {'taskId': taskId, 'toId': toId, 'ok': ok});
|
||||
},
|
||||
),
|
||||
// T-180: full team chat pane opened as a workspace tab.
|
||||
// Shares the TeamChatModel with the sidebar widget.
|
||||
TabContribution(
|
||||
id: 'claude.team-chat',
|
||||
slot: Slots.workspace,
|
||||
title: 'Team Chat',
|
||||
titleKey: 'tab.title',
|
||||
i18nNamespace: id,
|
||||
priority: 85,
|
||||
build: (_) {
|
||||
final orch = _orchestrator;
|
||||
if (orch == null) return const SizedBox.shrink();
|
||||
return TeamChatPane(model: orch.chatModel, broker: orch.broker);
|
||||
},
|
||||
),
|
||||
// CLI parity: open the team chat pane from the shell.
|
||||
// Usage: clide claude.team-chat.open
|
||||
CommandContribution(
|
||||
id: 'claude.team-chat.open',
|
||||
command: 'claude.team-chat.open',
|
||||
title: 'Claude: open the team chat pane',
|
||||
run: (args) async {
|
||||
_ctx?.panels.activateTab(Slots.workspace, 'claude.team-chat');
|
||||
return IpcResponse.ok(id: '', data: const {'status': 'opened'});
|
||||
},
|
||||
),
|
||||
// Usage: clide claude.team-chat.post [@name] <text...>
|
||||
// Posts a message into the broker channel as the user.
|
||||
// Leading @name tag selects the recipient; omit for broadcast.
|
||||
CommandContribution(
|
||||
id: 'claude.team-chat.post',
|
||||
command: 'claude.team-chat.post',
|
||||
title: 'Claude: post a message into the team channel as the user',
|
||||
run: (args) async {
|
||||
if (args.isEmpty) return IpcResponse.ok(id: '', data: const {'error': 'usage: [@name] <text>'});
|
||||
final raw = args.join(' ');
|
||||
String? recipient;
|
||||
String body = raw;
|
||||
if (raw.startsWith('@')) {
|
||||
final ws = raw.indexOf(RegExp(r'\s'));
|
||||
if (ws > 0) {
|
||||
final tag = raw.substring(1, ws);
|
||||
recipient = (tag == 'team' || tag.isEmpty) ? null : tag;
|
||||
body = raw.substring(ws).trim();
|
||||
}
|
||||
}
|
||||
_orchestrator?.chatModel.postAsUser(body, toName: recipient);
|
||||
return IpcResponse.ok(id: '', data: {'status': 'posted', 'to': ?recipient});
|
||||
},
|
||||
),
|
||||
// claude.agent.fork: branch a managed session into a new fork session
|
||||
// (T-172, D-6 CLI/UI parity for the roster fork button).
|
||||
// Usage: clide claude.agent.fork <sourceSessionId> [<cwd>]
|
||||
// <sourceSessionId>: the clide-internal id of the session to fork.
|
||||
// <cwd>: optional working directory; defaults to the source session's cwd.
|
||||
CommandContribution(
|
||||
id: 'claude.agent.fork',
|
||||
command: 'claude.agent.fork',
|
||||
title: 'Claude: fork a managed session into a new branch session',
|
||||
run: (args) async {
|
||||
final sourceId = args.firstOrNull;
|
||||
if (sourceId == null) {
|
||||
return IpcResponse.ok(id: '', data: const {'error': 'usage: claude.agent.fork <sourceSessionId> [<cwd>]'});
|
||||
}
|
||||
final orch = _orchestrator;
|
||||
if (orch == null) {
|
||||
return IpcResponse.ok(id: '', data: const {'error': 'orchestrator unavailable'});
|
||||
}
|
||||
final source = orch.byId(sourceId);
|
||||
if (source == null) {
|
||||
return IpcResponse.ok(id: '', data: {'error': 'unknown session "$sourceId"'});
|
||||
}
|
||||
final cwd = args.length >= 2 ? args[1] : source.cwd;
|
||||
final forkId = 'fork:$sourceId-${DateTime.now().millisecondsSinceEpoch}';
|
||||
await orch.spawn(SpawnSpec(id: forkId, role: 'fork of $sourceId', sessionId: forkId, cwd: cwd, forkSourceSessionId: source.sessionId));
|
||||
return IpcResponse.ok(id: '', data: {'forkId': forkId, 'sourceId': sourceId, 'status': 'spawned'});
|
||||
},
|
||||
),
|
||||
// Always-pickable left-panel tab: Claude activity (from
|
||||
// stats-cache.json) + the team roster when a team is running (T-141).
|
||||
TabContribution(
|
||||
id: 'claude.meta',
|
||||
slot: Slots.sidebar,
|
||||
title: 'Activity',
|
||||
icon: PhosphorIcons.byName('robot'),
|
||||
priority: 60,
|
||||
build: (_) => const ClaudeMetaSidebar(),
|
||||
),
|
||||
// In-pane status slot (T-145): the active Claude pane publishes
|
||||
// its model · permission-mode · context line here.
|
||||
// flex: 1 → StatusbarHost wraps this in Flexible(loose) so the slot
|
||||
// yields width under pressure and ClideMarquee scrolls (T-160).
|
||||
StatusItemContribution(id: 'claude.status-context', priority: 50, flex: 1, build: (_) => const PaneContextStatusItem()),
|
||||
];
|
||||
|
||||
@override
|
||||
Future<void> activate(ClideExtensionContext ctx) async {
|
||||
@@ -399,13 +388,15 @@ class ClaudeExtension extends ClideExtension {
|
||||
}
|
||||
final target = _orchestrator?.byId('primary') ?? _orchestrator?.visibleSessions.firstOrNull;
|
||||
if (target == null) return;
|
||||
target.conversation.inject(ImageMessage(
|
||||
uuid: 'image-${DateTime.now().microsecondsSinceEpoch}',
|
||||
timestamp: DateTime.now(),
|
||||
isSidechain: false,
|
||||
path: path,
|
||||
caption: m.data['caption'] as String?,
|
||||
));
|
||||
target.conversation.inject(
|
||||
ImageMessage(
|
||||
uuid: 'image-${DateTime.now().microsecondsSinceEpoch}',
|
||||
timestamp: DateTime.now(),
|
||||
isSidechain: false,
|
||||
path: path,
|
||||
caption: m.data['caption'] as String?,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -466,9 +457,7 @@ class ClaudeExtension extends ClideExtension {
|
||||
if (root == null || home == null) return IpcResponse.ok(id: '', data: const {});
|
||||
final dir = Directory('$home/.claude/projects/${root.replaceAll('/', '-')}');
|
||||
final sessions = await listSessions(dir);
|
||||
await ctx.dialog.show<Object>(
|
||||
(c, dismiss) => SessionStorageDialog(dir: dir, sessions: sessions, onClose: dismiss),
|
||||
);
|
||||
await ctx.dialog.show<Object>((c, dismiss) => SessionStorageDialog(dir: dir, sessions: sessions, onClose: dismiss));
|
||||
return IpcResponse.ok(id: '', data: const {'status': 'shown'});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,15 +17,11 @@ import 'package:flutter/widgets.dart';
|
||||
/// Open [path] full-size in the lightbox via the kernel dialog router.
|
||||
void openImageLightbox(BuildContext context, String path) {
|
||||
ClideKernel.of(context).dialog.show<Object>(
|
||||
(ctx, dismiss) => ClideLightbox(
|
||||
onDismiss: dismiss,
|
||||
child: Image(
|
||||
image: ClideFileImage(path),
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (ctx, _, __) => _placeholder(ctx, 48),
|
||||
),
|
||||
),
|
||||
);
|
||||
(ctx, dismiss) => ClideLightbox(
|
||||
onDismiss: dismiss,
|
||||
child: Image(image: ClideFileImage(path), fit: BoxFit.contain, errorBuilder: (ctx, _, _) => _placeholder(ctx, 48)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _placeholder(BuildContext context, double size) {
|
||||
@@ -64,13 +60,7 @@ class ImageThumbnail extends StatelessWidget {
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
child: Image(
|
||||
image: ClideFileImage(path),
|
||||
width: size,
|
||||
height: size,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (ctx, _, __) => _placeholder(ctx, size),
|
||||
),
|
||||
child: Image(image: ClideFileImage(path), width: size, height: size, fit: BoxFit.cover, errorBuilder: (ctx, _, _) => _placeholder(ctx, size)),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -67,24 +67,24 @@ class _PermissionModeControlState extends State<PermissionModeControl> {
|
||||
}
|
||||
|
||||
List<ClideMenuEntry> _entries(SurfaceTokens tokens) => [
|
||||
for (final m in kSafePermissionCycle)
|
||||
ClideMenuItem(
|
||||
leading: permissionModeIcon(m),
|
||||
color: permissionModeColor(m, tokens),
|
||||
label: permissionModeLabel(m),
|
||||
active: m == widget.mode,
|
||||
onSelect: () => widget.onSelect(m),
|
||||
),
|
||||
const ClideMenuSeparator(),
|
||||
ClideMenuItem(
|
||||
leading: permissionModeIcon('bypassPermissions'),
|
||||
color: permissionModeColor('bypassPermissions', tokens),
|
||||
label: permissionModeLabel('bypassPermissions'),
|
||||
enabled: false,
|
||||
active: widget.mode == 'bypassPermissions',
|
||||
onSelect: () {},
|
||||
),
|
||||
];
|
||||
for (final m in kSafePermissionCycle)
|
||||
ClideMenuItem(
|
||||
leading: permissionModeIcon(m),
|
||||
color: permissionModeColor(m, tokens),
|
||||
label: permissionModeLabel(m),
|
||||
active: m == widget.mode,
|
||||
onSelect: () => widget.onSelect(m),
|
||||
),
|
||||
const ClideMenuSeparator(),
|
||||
ClideMenuItem(
|
||||
leading: permissionModeIcon('bypassPermissions'),
|
||||
color: permissionModeColor('bypassPermissions', tokens),
|
||||
label: permissionModeLabel('bypassPermissions'),
|
||||
enabled: false,
|
||||
active: widget.mode == 'bypassPermissions',
|
||||
onSelect: () {},
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -94,11 +94,7 @@ class _PermissionModeControlState extends State<PermissionModeControl> {
|
||||
side: ClideAnchorSide.above,
|
||||
align: ClideAnchorAlign.end,
|
||||
offset: const Offset(0, -6),
|
||||
overlayBuilder: (ctx, ctrl) => ClideMenu(
|
||||
onClose: ctrl.close,
|
||||
minWidth: 180,
|
||||
entries: _entries(ClideTheme.of(ctx).surface),
|
||||
),
|
||||
overlayBuilder: (ctx, ctrl) => ClideMenu(onClose: ctrl.close, minWidth: 180, entries: _entries(ClideTheme.of(ctx).surface)),
|
||||
anchor: ListenableBuilder(
|
||||
listenable: _overlay,
|
||||
builder: (ctx, _) {
|
||||
@@ -116,9 +112,7 @@ class _PermissionModeControlState extends State<PermissionModeControl> {
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: hovered ? tokens.listItemHoverBackground : null,
|
||||
border: Border.all(
|
||||
color: open ? tokens.globalFocus : (hovered ? tokens.panelActiveBorder : tokens.globalBorder),
|
||||
),
|
||||
border: Border.all(color: open ? tokens.globalFocus : (hovered ? tokens.panelActiveBorder : tokens.globalBorder)),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: ClideIcon(permissionModeIcon(widget.mode), size: 16, color: permissionModeColor(widget.mode, tokens)),
|
||||
|
||||
@@ -27,7 +27,8 @@ const _kOther = '\u0000other';
|
||||
/// Preformatted note for the "Deny & simplify" permission option (T-311): deny
|
||||
/// THIS action and ask Claude to reformulate it more simply, explicitly without
|
||||
/// touching the permission surface (memories / settings).
|
||||
const _kDenySimplifyNote = 'Denied — this action is too complex for the permission system to approve cleanly. '
|
||||
const _kDenySimplifyNote =
|
||||
'Denied — this action is too complex for the permission system to approve cleanly. '
|
||||
'Please retry with a simpler, more granular approach (break it into smaller steps or use a plainer command) '
|
||||
'to avoid this permission prompt. This is a one-off for THIS action only: do not add a memory and do not '
|
||||
'change permission settings — just reformulate and try again. Do not narrate or explain the change; '
|
||||
@@ -295,11 +296,13 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
|
||||
[
|
||||
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),
|
||||
const Spacer(),
|
||||
_chatInstead(tokens),
|
||||
]),
|
||||
Row(
|
||||
children: [
|
||||
ClideButton(label: 'Submit', variant: ClideButtonVariant.primary, onPressed: (_q.isNotEmpty && _answer(0).isNotEmpty) ? _submit : null),
|
||||
const Spacer(),
|
||||
_chatInstead(tokens),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -312,11 +315,13 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
|
||||
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)),
|
||||
const SizedBox(width: 8),
|
||||
ClideButton(label: 'Submit answers', variant: ClideButtonVariant.primary, onPressed: _allAnswered ? _submit : null),
|
||||
]),
|
||||
Row(
|
||||
children: [
|
||||
ClideButton(label: '‹ Back', onPressed: () => setState(() => _step = _q.length - 1)),
|
||||
const SizedBox(width: 8),
|
||||
ClideButton(label: 'Submit answers', variant: ClideButtonVariant.primary, onPressed: _allAnswered ? _submit : null),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -330,19 +335,14 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
|
||||
const SizedBox(height: 10),
|
||||
_questionBody(tokens, _step),
|
||||
const SizedBox(height: 6),
|
||||
Row(children: [
|
||||
if (_step > 0) ...[
|
||||
ClideButton(label: '‹ Back', onPressed: () => setState(() => _step--)),
|
||||
const SizedBox(width: 8),
|
||||
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),
|
||||
const Spacer(),
|
||||
_chatInstead(tokens),
|
||||
],
|
||||
ClideButton(
|
||||
label: last ? 'Review ›' : 'Next ›',
|
||||
variant: ClideButtonVariant.primary,
|
||||
onPressed: answered ? () => setState(() => _step++) : null,
|
||||
),
|
||||
const Spacer(),
|
||||
_chatInstead(tokens),
|
||||
]),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -354,12 +354,17 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
|
||||
final done = _answer(i).isNotEmpty;
|
||||
final text = '${i + 1} · $head${done ? ' ✓' : ''}';
|
||||
if (i == _step) {
|
||||
chips.add(Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: tokens.globalFocus.withValues(alpha: 0.18), border: Border.all(color: tokens.statusInfo), borderRadius: BorderRadius.circular(4)),
|
||||
child: ClideText(text, fontSize: clideFontMeta, fontFamily: clideMonoFamily, color: tokens.globalForeground),
|
||||
));
|
||||
chips.add(
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: tokens.globalFocus.withValues(alpha: 0.18),
|
||||
border: Border.all(color: tokens.statusInfo),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: ClideText(text, fontSize: clideFontMeta, fontFamily: clideMonoFamily, color: tokens.globalForeground),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
chips.add(ClideText(text, fontSize: clideFontMeta, fontFamily: clideMonoFamily, color: done ? tokens.statusSuccess : tokens.globalTextMuted));
|
||||
}
|
||||
@@ -376,8 +381,13 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(width: 110, child: ClideText(head, fontSize: clideFontMeta, color: tokens.globalTextMuted)),
|
||||
Expanded(child: ClideText('→ ${_answer(qi)}', fontSize: clideFontSmall, color: tokens.globalForeground)),
|
||||
SizedBox(
|
||||
width: 110,
|
||||
child: ClideText(head, fontSize: clideFontMeta, color: tokens.globalTextMuted),
|
||||
),
|
||||
Expanded(
|
||||
child: ClideText('→ ${_answer(qi)}', fontSize: clideFontSmall, color: tokens.globalForeground),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -403,10 +413,7 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
|
||||
_optButton(qi, _kOther, '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: 'type your answer…')],
|
||||
const SizedBox(height: 8),
|
||||
_NoteField(controller: _qnote[qi], placeholder: '+ note (optional)'),
|
||||
],
|
||||
@@ -499,10 +506,7 @@ Widget toolInputBody(SurfaceTokens tokens, String toolName, Map<String, dynamic>
|
||||
/// / timeout annotations.
|
||||
Widget toolBashBody(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) 'background', if (input['timeout'] is num) 'timeout ${input['timeout']}ms'];
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -564,19 +568,14 @@ Widget toolReadLikeBody(SurfaceTokens tokens, String toolName, Map<String, dynam
|
||||
if (pat != null && pat.isNotEmpty) extra.add('"$pat"');
|
||||
}
|
||||
final label = [path.toString(), ...extra].where((s) => s.isNotEmpty).join(' ');
|
||||
return ClideText(
|
||||
label.isNotEmpty ? label : toolName,
|
||||
fontSize: clideFontMeta,
|
||||
fontFamily: clideMonoFamily,
|
||||
color: tokens.globalForeground,
|
||||
);
|
||||
return ClideText(label.isNotEmpty ? label : toolName, fontSize: clideFontMeta, fontFamily: clideMonoFamily, color: tokens.globalForeground);
|
||||
}
|
||||
|
||||
/// A muted file path line, shared across tool bodies.
|
||||
Widget toolPathLine(SurfaceTokens tokens, String path) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child: ClideText(path, fontSize: clideFontMeta, fontFamily: clideMonoFamily, color: tokens.globalTextMuted),
|
||||
);
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child: ClideText(path, fontSize: clideFontMeta, fontFamily: clideMonoFamily, color: tokens.globalTextMuted),
|
||||
);
|
||||
|
||||
// -- shared note / free-text field -------------------------------------------
|
||||
|
||||
@@ -613,7 +612,7 @@ class _NoteFieldState extends State<_NoteField> {
|
||||
children: [
|
||||
ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: widget.controller,
|
||||
builder: (_, v, __) => v.text.isEmpty ? ClideText(widget.placeholder, muted: true, fontSize: clideFontSmall) : const SizedBox.shrink(),
|
||||
builder: (_, v, _) => v.text.isEmpty ? ClideText(widget.placeholder, muted: true, fontSize: clideFontSmall) : const SizedBox.shrink(),
|
||||
),
|
||||
EditableText(
|
||||
controller: widget.controller,
|
||||
@@ -652,14 +651,9 @@ List<_Question> _parseQuestions(Map<String, dynamic> input) {
|
||||
return [
|
||||
for (final q in raw)
|
||||
if (q is Map)
|
||||
_Question(
|
||||
q['question'] as String? ?? '',
|
||||
q['header'] as String? ?? '',
|
||||
q['multiSelect'] as bool? ?? false,
|
||||
[
|
||||
for (final o in (q['options'] as List? ?? const []))
|
||||
if (o is Map) _Option(o['label'] as String? ?? '', o['description'] as String? ?? ''),
|
||||
],
|
||||
),
|
||||
_Question(q['question'] as String? ?? '', q['header'] as String? ?? '', q['multiSelect'] as bool? ?? false, [
|
||||
for (final o in (q['options'] as List? ?? const []))
|
||||
if (o is Map) _Option(o['label'] as String? ?? '', o['description'] as String? ?? ''),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -13,13 +13,7 @@ import 'dart:io';
|
||||
|
||||
/// One session in the workspace, summarised for the picker.
|
||||
class SessionSummary {
|
||||
const SessionSummary({
|
||||
required this.id,
|
||||
required this.modified,
|
||||
this.firstUser,
|
||||
this.lastUser,
|
||||
this.sizeBytes = 0,
|
||||
});
|
||||
const SessionSummary({required this.id, required this.modified, this.firstUser, this.lastUser, this.sizeBytes = 0});
|
||||
|
||||
/// The session id (the `<uuid>` of `<uuid>.jsonl`).
|
||||
final String id;
|
||||
@@ -94,11 +88,7 @@ String? _userTextOf(String line) {
|
||||
/// Sessions in [dir] (the munged project dir), most-recently-modified first,
|
||||
/// capped at [max]. Each is summarised by bookend user prompts read from a
|
||||
/// bounded [window] at each end of its transcript.
|
||||
Future<List<SessionSummary>> listSessions(
|
||||
Directory dir, {
|
||||
int max = 20,
|
||||
int window = 128 * 1024,
|
||||
}) async {
|
||||
Future<List<SessionSummary>> listSessions(Directory dir, {int max = 20, int window = 128 * 1024}) async {
|
||||
if (!await dir.exists()) return const [];
|
||||
final files = <File>[];
|
||||
await for (final e in dir.list(followLinks: false)) {
|
||||
@@ -109,13 +99,15 @@ Future<List<SessionSummary>> listSessions(
|
||||
final stat = await f.stat();
|
||||
final bookends = await _bookends(f, window);
|
||||
final id = _sessionId(f.path);
|
||||
summaries.add(SessionSummary(
|
||||
id: id,
|
||||
modified: stat.modified,
|
||||
firstUser: bookends.first,
|
||||
lastUser: bookends.last,
|
||||
sizeBytes: stat.size + await _dirSize(Directory('${dir.path}/$id')),
|
||||
));
|
||||
summaries.add(
|
||||
SessionSummary(
|
||||
id: id,
|
||||
modified: stat.modified,
|
||||
firstUser: bookends.first,
|
||||
lastUser: bookends.last,
|
||||
sizeBytes: stat.size + await _dirSize(Directory('${dir.path}/$id')),
|
||||
),
|
||||
);
|
||||
}
|
||||
summaries.sort((a, b) => b.modified.compareTo(a.modified));
|
||||
return summaries.length > max ? summaries.sublist(0, max) : summaries;
|
||||
|
||||
@@ -32,11 +32,7 @@ const _resumeTailBytes = 256 * 1024;
|
||||
|
||||
/// Creates the subprocess for a session — production uses
|
||||
/// [ClaudeStreamJsonProcess.start]; tests inject a fake.
|
||||
typedef ProcessFactory = Future<StreamJsonProcess> Function({
|
||||
required List<String> sessionArgs,
|
||||
required String cwd,
|
||||
Map<String, String>? env,
|
||||
});
|
||||
typedef ProcessFactory = Future<StreamJsonProcess> Function({required List<String> sessionArgs, required String cwd, Map<String, String>? env});
|
||||
|
||||
/// What to spawn. [id] is the orchestrator's stable key (e.g. `primary`,
|
||||
/// `teammate:tyre`); [sessionId] is claude's `--session-id`.
|
||||
@@ -150,10 +146,7 @@ ClaudeSessionOrchestrator? activeSessionOrchestrator;
|
||||
|
||||
class ClaudeSessionOrchestrator extends ChangeNotifier {
|
||||
ClaudeSessionOrchestrator({ProcessFactory? processFactory}) : _factory = processFactory ?? _spawnClaude {
|
||||
_chatModel = TeamChatModel(
|
||||
broker: broker,
|
||||
sessionResolver: (name) => byMemberName(name)?.session,
|
||||
);
|
||||
_chatModel = TeamChatModel(broker: broker, sessionResolver: (name) => byMemberName(name)?.session);
|
||||
}
|
||||
|
||||
final ProcessFactory _factory;
|
||||
@@ -181,9 +174,9 @@ class ClaudeSessionOrchestrator extends ChangeNotifier {
|
||||
|
||||
/// Just the sessions a pane should currently render.
|
||||
List<ManagedSession> get visibleSessions => [
|
||||
for (final m in _sessions.values)
|
||||
if (m.visible) m,
|
||||
];
|
||||
for (final m in _sessions.values)
|
||||
if (m.visible) m,
|
||||
];
|
||||
|
||||
ManagedSession? byId(String id) => _sessions[id];
|
||||
|
||||
@@ -224,18 +217,9 @@ class ClaudeSessionOrchestrator extends ChangeNotifier {
|
||||
preambles.add(_teamSystemPrompt(name, spec.role));
|
||||
}
|
||||
final bootstrap = agentBootstrap(spec.cwd, base: spec.env);
|
||||
sessionArgs = [
|
||||
'--append-system-prompt',
|
||||
preambles.join('\n\n'),
|
||||
...bootstrap.extraArgs,
|
||||
...sessionArgs,
|
||||
];
|
||||
sessionArgs = ['--append-system-prompt', preambles.join('\n\n'), ...bootstrap.extraArgs, ...sessionArgs];
|
||||
|
||||
final proc = await _factory(
|
||||
sessionArgs: sessionArgs,
|
||||
cwd: spec.cwd,
|
||||
env: bootstrap.envDelta,
|
||||
);
|
||||
final proc = await _factory(sessionArgs: sessionArgs, cwd: spec.cwd, env: bootstrap.envDelta);
|
||||
final session = StreamJsonSession(proc, mcpServers: mcpServers)..start();
|
||||
final seed = spec.resume && spec.transcriptPath != null ? await _readTranscriptTail(spec.transcriptPath!) : null;
|
||||
final conversation = ConversationController(stream: session.items, seed: seed, onDispose: session.dispose);
|
||||
@@ -362,7 +346,8 @@ class ClaudeSessionOrchestrator extends ChangeNotifier {
|
||||
}
|
||||
|
||||
/// The team-awareness preamble injected via `--append-system-prompt` (T-170).
|
||||
static String _teamSystemPrompt(String name, String role) => 'You are part of a clide-managed agent team. Your name is "$name" and your role is "$role". '
|
||||
static String _teamSystemPrompt(String name, String role) =>
|
||||
'You are part of a clide-managed agent team. Your name is "$name" and your role is "$role". '
|
||||
'Coordinate with teammates using the clide-team MCP tools: '
|
||||
'send_message(to, text) to message one teammate by name, broadcast(text) to message all, '
|
||||
'list_teammates() to see the roster, inbox() to read messages sent to you, and '
|
||||
|
||||
@@ -12,12 +12,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class SessionPickerDialog extends StatefulWidget {
|
||||
const SessionPickerDialog({
|
||||
super.key,
|
||||
required this.sessions,
|
||||
required this.onPick,
|
||||
required this.onCancel,
|
||||
});
|
||||
const SessionPickerDialog({super.key, required this.sessions, required this.onPick, required this.onCancel});
|
||||
|
||||
final List<SessionSummary> sessions;
|
||||
final void Function(String id) onPick;
|
||||
@@ -92,11 +87,7 @@ class _SessionPickerDialogState extends State<SessionPickerDialog> {
|
||||
)
|
||||
else
|
||||
Flexible(
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: widget.sessions.length,
|
||||
itemBuilder: (ctx, i) => _row(theme, i),
|
||||
),
|
||||
child: ListView.builder(shrinkWrap: true, itemCount: widget.sessions.length, itemBuilder: (ctx, i) => _row(theme, i)),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -117,13 +108,7 @@ class _SessionPickerDialogState extends State<SessionPickerDialog> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClideText(
|
||||
s.label,
|
||||
fontSize: clideFontSmall,
|
||||
color: theme.globalForeground,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
ClideText(s.label, fontSize: clideFontSmall, color: theme.globalForeground, maxLines: 2, overflow: TextOverflow.ellipsis),
|
||||
const SizedBox(height: 2),
|
||||
ClideText(relativeTime(s.modified), muted: true, fontSize: clideFontSmall),
|
||||
],
|
||||
|
||||
@@ -17,13 +17,7 @@ import 'package:flutter/widgets.dart';
|
||||
typedef SessionDeleter = Future<void> Function(Directory dir, String id);
|
||||
|
||||
class SessionStorageDialog extends StatefulWidget {
|
||||
const SessionStorageDialog({
|
||||
super.key,
|
||||
required this.dir,
|
||||
required this.sessions,
|
||||
required this.onClose,
|
||||
this.deleter = deleteSession,
|
||||
});
|
||||
const SessionStorageDialog({super.key, required this.dir, required this.sessions, required this.onClose, this.deleter = deleteSession});
|
||||
|
||||
final Directory dir;
|
||||
final List<SessionSummary> sessions;
|
||||
@@ -79,19 +73,11 @@ class _SessionStorageDialogState extends State<SessionStorageDialog> {
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 12, 14, 4),
|
||||
child: ClideText(
|
||||
'Session storage · ${formatBytes(_total)} total',
|
||||
fontSize: clideFontBody,
|
||||
color: theme.globalForeground,
|
||||
),
|
||||
child: ClideText('Session storage · ${formatBytes(_total)} total', fontSize: clideFontBody, color: theme.globalForeground),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 0, 14, 8),
|
||||
child: ClideText(
|
||||
'Deleting a session you are currently using will break that pane.',
|
||||
muted: true,
|
||||
fontSize: clideFontSmall,
|
||||
),
|
||||
child: ClideText('Deleting a session you are currently using will break that pane.', muted: true, fontSize: clideFontSmall),
|
||||
),
|
||||
if (_sessions.isEmpty)
|
||||
Padding(
|
||||
@@ -100,11 +86,7 @@ class _SessionStorageDialogState extends State<SessionStorageDialog> {
|
||||
)
|
||||
else
|
||||
Flexible(
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: _sessions.length,
|
||||
itemBuilder: (ctx, i) => _row(theme, _sessions[i]),
|
||||
),
|
||||
child: ListView.builder(shrinkWrap: true, itemCount: _sessions.length, itemBuilder: (ctx, i) => _row(theme, _sessions[i])),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -40,11 +40,7 @@ class ClaudeStreamJsonProcess implements StreamJsonProcess {
|
||||
|
||||
/// Spawn `claude` in stream-json mode. [sessionArgs] is `['--session-id', id]`
|
||||
/// for a new session or `['--resume', id]` to resume an existing one (T-161).
|
||||
static Future<ClaudeStreamJsonProcess> start({
|
||||
required List<String> sessionArgs,
|
||||
required String cwd,
|
||||
Map<String, String>? env,
|
||||
}) async {
|
||||
static Future<ClaudeStreamJsonProcess> start({required List<String> sessionArgs, required String cwd, Map<String, String>? env}) async {
|
||||
final proc = await Process.start(
|
||||
'claude',
|
||||
[
|
||||
@@ -176,10 +172,10 @@ final class AllowTool extends ToolDecision {
|
||||
final String? followUpNote;
|
||||
@override
|
||||
Map<String, dynamic> toJson() => {
|
||||
'behavior': 'allow',
|
||||
'updatedInput': updatedInput,
|
||||
if (updatedPermissions != null && updatedPermissions!.isNotEmpty) 'updatedPermissions': updatedPermissions,
|
||||
};
|
||||
'behavior': 'allow',
|
||||
'updatedInput': updatedInput,
|
||||
if (updatedPermissions != null && updatedPermissions!.isNotEmpty) 'updatedPermissions': updatedPermissions,
|
||||
};
|
||||
}
|
||||
|
||||
/// Deny the tool with a user-facing [message] (required by the protocol).
|
||||
@@ -310,15 +306,17 @@ class StreamJsonSession {
|
||||
// makes claude drive their JSON-RPC over `mcp_message` (T-170). Only sent
|
||||
// when we actually host a server, so a plain session is unchanged.
|
||||
if (_mcpServers.isNotEmpty) {
|
||||
_proc.writeLine(jsonEncode({
|
||||
'type': 'control_request',
|
||||
'request_id': 'init-${_localSeq++}',
|
||||
'request': {
|
||||
'subtype': 'initialize',
|
||||
'hooks': <String, dynamic>{},
|
||||
'sdkMcpServers': [for (final s in _mcpServers) s.name],
|
||||
},
|
||||
}));
|
||||
_proc.writeLine(
|
||||
jsonEncode({
|
||||
'type': 'control_request',
|
||||
'request_id': 'init-${_localSeq++}',
|
||||
'request': {
|
||||
'subtype': 'initialize',
|
||||
'hooks': <String, dynamic>{},
|
||||
'sdkMcpServers': [for (final s in _mcpServers) s.name],
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,15 +446,17 @@ class StreamJsonSession {
|
||||
final input = (request['input'] as Map?)?.cast<String, dynamic>() ?? <String, dynamic>{};
|
||||
final tuid = request['tool_use_id'] as String? ?? '';
|
||||
if (tuid.isNotEmpty) _promptedToolUses.add(tuid);
|
||||
_queue.add(ToolPrompt(
|
||||
promptId: rid,
|
||||
toolName: toolName,
|
||||
displayName: request['display_name'] as String? ?? toolName,
|
||||
description: request['description'] as String?,
|
||||
toolUseId: request['tool_use_id'] as String? ?? '',
|
||||
input: input,
|
||||
permissionSuggestions: (request['permission_suggestions'] as List?) ?? const [],
|
||||
));
|
||||
_queue.add(
|
||||
ToolPrompt(
|
||||
promptId: rid,
|
||||
toolName: toolName,
|
||||
displayName: request['display_name'] as String? ?? toolName,
|
||||
description: request['description'] as String?,
|
||||
toolUseId: request['tool_use_id'] as String? ?? '',
|
||||
input: input,
|
||||
permissionSuggestions: (request['permission_suggestions'] as List?) ?? const [],
|
||||
),
|
||||
);
|
||||
_pendingCtl.add(pendingPrompt);
|
||||
return; // awaits resolvePrompt
|
||||
}
|
||||
@@ -465,10 +465,12 @@ class StreamJsonSession {
|
||||
unawaited(_handleMcpMessage(rid, request.cast<String, dynamic>()));
|
||||
return;
|
||||
}
|
||||
_proc.writeLine(jsonEncode({
|
||||
'type': 'control_response',
|
||||
'response': {'subtype': 'error', 'request_id': rid, 'error': 'Unsupported control request subtype: ${request['subtype']}'},
|
||||
}));
|
||||
_proc.writeLine(
|
||||
jsonEncode({
|
||||
'type': 'control_response',
|
||||
'response': {'subtype': 'error', 'request_id': rid, 'error': 'Unsupported control request subtype: ${request['subtype']}'},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/// Answer an `mcp_message` control_request: dispatch its JSON-RPC to the named
|
||||
@@ -489,14 +491,16 @@ class StreamJsonSession {
|
||||
} else {
|
||||
mcpResponse = await _dispatchMcp(server, message);
|
||||
}
|
||||
_proc.writeLine(jsonEncode({
|
||||
'type': 'control_response',
|
||||
'response': {
|
||||
'subtype': 'success',
|
||||
'request_id': rid,
|
||||
'response': {'mcp_response': mcpResponse}
|
||||
},
|
||||
}));
|
||||
_proc.writeLine(
|
||||
jsonEncode({
|
||||
'type': 'control_response',
|
||||
'response': {
|
||||
'subtype': 'success',
|
||||
'request_id': rid,
|
||||
'response': {'mcp_response': mcpResponse},
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
McpServer? _mcpServerNamed(String? name) {
|
||||
@@ -559,10 +563,12 @@ class StreamJsonSession {
|
||||
_toolUseOutcome[prompt.toolUseId] = decision is AllowTool;
|
||||
if (decision is DenyTool && decision.quiet) _quietErrorToolUses.add(prompt.toolUseId);
|
||||
}
|
||||
_proc.writeLine(jsonEncode({
|
||||
'type': 'control_response',
|
||||
'response': {'subtype': 'success', 'request_id': promptId, 'response': decision.toJson()},
|
||||
}));
|
||||
_proc.writeLine(
|
||||
jsonEncode({
|
||||
'type': 'control_response',
|
||||
'response': {'subtype': 'success', 'request_id': promptId, 'response': decision.toJson()},
|
||||
}),
|
||||
);
|
||||
if (decision is AllowTool) {
|
||||
// The prompt card is ephemeral (it vanishes once resolved), so leave a
|
||||
// compact record of an answered question in the conversation log (D-78).
|
||||
@@ -652,16 +658,13 @@ class StreamJsonSession {
|
||||
/// so it renders immediately (stream-json doesn't replay stdin without
|
||||
/// `--replay-user-messages`).
|
||||
void send(String text) {
|
||||
_proc.writeLine(jsonEncode({
|
||||
'type': 'user',
|
||||
'message': {'role': 'user', 'content': text},
|
||||
}));
|
||||
_items.add(UserMessage(
|
||||
uuid: 'local-${_localSeq++}',
|
||||
timestamp: DateTime.now(),
|
||||
isSidechain: false,
|
||||
text: text,
|
||||
));
|
||||
_proc.writeLine(
|
||||
jsonEncode({
|
||||
'type': 'user',
|
||||
'message': {'role': 'user', 'content': text},
|
||||
}),
|
||||
);
|
||||
_items.add(UserMessage(uuid: 'local-${_localSeq++}', timestamp: DateTime.now(), isSidechain: false, text: text));
|
||||
_setBusy(true);
|
||||
}
|
||||
|
||||
@@ -669,11 +672,13 @@ class StreamJsonSession {
|
||||
/// the `interrupt` control_request; claude cancels the current turn and ends
|
||||
/// it with a `result`, which clears [busy]. Safe to call when idle.
|
||||
void interrupt() {
|
||||
_proc.writeLine(jsonEncode({
|
||||
'type': 'control_request',
|
||||
'request_id': 'interrupt-${_localSeq++}',
|
||||
'request': {'subtype': 'interrupt'},
|
||||
}));
|
||||
_proc.writeLine(
|
||||
jsonEncode({
|
||||
'type': 'control_request',
|
||||
'request_id': 'interrupt-${_localSeq++}',
|
||||
'request': {'subtype': 'interrupt'},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/// Set the session's permission mode (T-181, D-77). Sends a
|
||||
@@ -685,11 +690,13 @@ class StreamJsonSession {
|
||||
/// cockpit badge's plain click; bypassPermissions is reachable only via a
|
||||
/// confirmed shift-click (T-181).
|
||||
void setPermissionMode(String mode) {
|
||||
_proc.writeLine(jsonEncode({
|
||||
'type': 'control_request',
|
||||
'request_id': 'set-perm-${_localSeq++}',
|
||||
'request': {'subtype': 'set_permission_mode', 'mode': mode},
|
||||
}));
|
||||
_proc.writeLine(
|
||||
jsonEncode({
|
||||
'type': 'control_request',
|
||||
'request_id': 'set-perm-${_localSeq++}',
|
||||
'request': {'subtype': 'set_permission_mode', 'mode': mode},
|
||||
}),
|
||||
);
|
||||
// Optimistically reflect the change so the badge / status line update
|
||||
// immediately (T-250) — the control_request emits no status event, and a
|
||||
// fresh system/init only arrives later. The next init reconciles if the
|
||||
|
||||
@@ -47,7 +47,7 @@ List<TaskItem> taskListFrom(List<ConversationItem> items) {
|
||||
}
|
||||
|
||||
TaskStatus _statusFrom(Object? raw) => switch (raw) {
|
||||
'in_progress' => TaskStatus.inProgress,
|
||||
'completed' => TaskStatus.completed,
|
||||
_ => TaskStatus.pending,
|
||||
};
|
||||
'in_progress' => TaskStatus.inProgress,
|
||||
'completed' => TaskStatus.completed,
|
||||
_ => TaskStatus.pending,
|
||||
};
|
||||
|
||||
@@ -35,13 +35,7 @@ class TeamMemberRef {
|
||||
|
||||
/// A message left for a member, in arrival order.
|
||||
class TeamMessage {
|
||||
const TeamMessage({
|
||||
required this.from,
|
||||
required this.text,
|
||||
required this.at,
|
||||
this.to,
|
||||
this.broadcast = false,
|
||||
});
|
||||
const TeamMessage({required this.from, required this.text, required this.at, this.to, this.broadcast = false});
|
||||
final String from;
|
||||
|
||||
/// Recipient name: a single member's display name (direct message), `null`
|
||||
@@ -52,13 +46,7 @@ class TeamMessage {
|
||||
final DateTime at;
|
||||
final bool broadcast;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'from': from,
|
||||
if (to != null) 'to': to,
|
||||
'text': text,
|
||||
'at': at.toIso8601String(),
|
||||
if (broadcast) 'broadcast': true,
|
||||
};
|
||||
Map<String, dynamic> toJson() => {'from': from, if (to != null) 'to': to, 'text': text, 'at': at.toIso8601String(), if (broadcast) 'broadcast': true};
|
||||
}
|
||||
|
||||
/// A shared task. Status is one of `open` / `claimed` / `done`.
|
||||
@@ -69,12 +57,7 @@ class TeamTask {
|
||||
String status;
|
||||
String? owner;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'status': status,
|
||||
if (owner != null) 'owner': owner,
|
||||
};
|
||||
Map<String, dynamic> toJson() => {'id': id, 'title': title, 'status': status, if (owner != null) 'owner': owner};
|
||||
}
|
||||
|
||||
/// Pushes [text] into the member identified by [toMemberId] as a user message
|
||||
@@ -304,7 +287,7 @@ class TeamBroker {
|
||||
return {'ok': true, 'task': t.toJson()};
|
||||
}
|
||||
return {
|
||||
'tasks': [for (final t in _tasks.values) t.toJson()]
|
||||
'tasks': [for (final t in _tasks.values) t.toJson()],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -362,25 +345,26 @@ class TeamMcpServer implements McpServer {
|
||||
return _result(broker.claimTask(memberId, id: arguments['id'] as String?, title: arguments['title'] as String?));
|
||||
case 'task_status':
|
||||
return _result(
|
||||
broker.taskStatus(memberId, id: arguments['id'] as String?, status: arguments['status'] as String?, title: arguments['title'] as String?));
|
||||
broker.taskStatus(memberId, id: arguments['id'] as String?, status: arguments['status'] as String?, title: arguments['title'] as String?),
|
||||
);
|
||||
default:
|
||||
return _error('Unknown team tool: $name');
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> _result(Map<String, dynamic> value) => {
|
||||
'content': [
|
||||
{'type': 'text', 'text': jsonEncode(value)},
|
||||
],
|
||||
'isError': value['ok'] == false,
|
||||
};
|
||||
'content': [
|
||||
{'type': 'text', 'text': jsonEncode(value)},
|
||||
],
|
||||
'isError': value['ok'] == false,
|
||||
};
|
||||
|
||||
Map<String, dynamic> _error(String message) => {
|
||||
'content': [
|
||||
{'type': 'text', 'text': message},
|
||||
],
|
||||
'isError': true,
|
||||
};
|
||||
'content': [
|
||||
{'type': 'text', 'text': message},
|
||||
],
|
||||
'isError': true,
|
||||
};
|
||||
}
|
||||
|
||||
const _toolDefs = <Map<String, dynamic>>[
|
||||
|
||||
@@ -29,11 +29,7 @@ typedef SessionResolver = StreamJsonSession? Function(String memberName);
|
||||
/// Lifetime matches the orchestrator: created once, subscribed to the broker,
|
||||
/// disposed when the orchestrator is torn down.
|
||||
class TeamChatModel {
|
||||
TeamChatModel({
|
||||
required TeamBroker broker,
|
||||
SessionResolver? sessionResolver,
|
||||
}) : _broker = broker,
|
||||
_sessionResolver = sessionResolver {
|
||||
TeamChatModel({required TeamBroker broker, SessionResolver? sessionResolver}) : _broker = broker, _sessionResolver = sessionResolver {
|
||||
_sub = broker.messages.listen(_onMessage);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,12 +26,7 @@ import 'package:flutter/widgets.dart';
|
||||
/// [onPopOut] is called when the user taps the pop-out icon to open the full
|
||||
/// pane — the extension wires this to `panels.activateTab`.
|
||||
class TeamChatSidebar extends StatefulWidget {
|
||||
const TeamChatSidebar({
|
||||
super.key,
|
||||
required this.model,
|
||||
required this.broker,
|
||||
required this.onPopOut,
|
||||
});
|
||||
const TeamChatSidebar({super.key, required this.model, required this.broker, required this.onPopOut});
|
||||
|
||||
final TeamChatModel model;
|
||||
final TeamBroker broker;
|
||||
@@ -149,11 +144,7 @@ class _TeamChatSidebarState extends State<TeamChatSidebar> {
|
||||
onTap: widget.onPopOut,
|
||||
builder: (ctx, hovered, _) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 1),
|
||||
child: ClideIcon(
|
||||
PhosphorIcons.byName('arrows-out-simple'),
|
||||
size: 10,
|
||||
color: hovered ? tokens.globalForeground : tokens.globalTextMuted,
|
||||
),
|
||||
child: ClideIcon(PhosphorIcons.byName('arrows-out-simple'), size: 10, color: hovered ? tokens.globalForeground : tokens.globalTextMuted),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -177,13 +168,7 @@ 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: '@name or @team …'),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -200,11 +185,7 @@ class _TeamChatSidebarState extends State<TeamChatSidebar> {
|
||||
/// Reads from the same [TeamChatModel] as [TeamChatSidebar]. Supports the
|
||||
/// interrupt tickbox and full @-completion.
|
||||
class TeamChatPane extends StatefulWidget {
|
||||
const TeamChatPane({
|
||||
super.key,
|
||||
required this.model,
|
||||
required this.broker,
|
||||
});
|
||||
const TeamChatPane({super.key, required this.model, required this.broker});
|
||||
|
||||
final TeamChatModel model;
|
||||
final TeamBroker broker;
|
||||
@@ -231,11 +212,7 @@ class _TeamChatPaneState extends State<TeamChatPane> {
|
||||
// Scroll to bottom on new message.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_scrollController.hasClients) {
|
||||
_scrollController.animateTo(
|
||||
_scrollController.position.maxScrollExtent,
|
||||
duration: const Duration(milliseconds: 120),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
_scrollController.animateTo(_scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 120), curve: Curves.easeOut);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -297,11 +274,7 @@ class _TeamChatPaneState extends State<TeamChatPane> {
|
||||
final text = raw.trim();
|
||||
if (text.isEmpty) return;
|
||||
final parsed = parseAtTag(text);
|
||||
widget.model.postAsUser(
|
||||
parsed.body.isEmpty ? text : parsed.body,
|
||||
toName: parsed.recipient,
|
||||
interrupt: _interrupt,
|
||||
);
|
||||
widget.model.postAsUser(parsed.body.isEmpty ? text : parsed.body, toName: parsed.recipient, interrupt: _interrupt);
|
||||
_controller.clear();
|
||||
}
|
||||
|
||||
@@ -340,11 +313,7 @@ class _TeamChatPaneState extends State<TeamChatPane> {
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
itemCount: messages.length,
|
||||
itemBuilder: (_, i) => _ChatRow(
|
||||
key: ValueKey(messages[i].at.microsecondsSinceEpoch),
|
||||
message: messages[i],
|
||||
tokens: tokens,
|
||||
),
|
||||
itemBuilder: (_, i) => _ChatRow(key: ValueKey(messages[i].at.microsecondsSinceEpoch), message: messages[i], tokens: tokens),
|
||||
),
|
||||
),
|
||||
// Composer + interrupt tickbox.
|
||||
@@ -375,24 +344,13 @@ class _TeamChatPaneState extends State<TeamChatPane> {
|
||||
margin: const EdgeInsets.only(right: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: _interrupt ? tokens.globalFocus.withAlpha(40) : const Color(0x00000000),
|
||||
border: Border.all(
|
||||
color: _interrupt ? tokens.globalFocus : tokens.globalTextMuted,
|
||||
width: 1,
|
||||
),
|
||||
border: Border.all(color: _interrupt ? tokens.globalFocus : tokens.globalTextMuted, width: 1),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
child: _interrupt
|
||||
? Center(
|
||||
child: ClideIcon(PhosphorIcons.byName('check'), size: 9, color: tokens.globalFocus),
|
||||
)
|
||||
: null,
|
||||
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('Interrupt', fontSize: clideFontSmall, color: _interrupt ? tokens.globalForeground : tokens.globalTextMuted),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -405,13 +363,7 @@ 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: '@name or @team …'),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -439,8 +391,8 @@ class _ChatRow extends StatelessWidget {
|
||||
final toLabel = message.broadcast
|
||||
? '→ all'
|
||||
: message.to != null
|
||||
? '→ ${message.to}'
|
||||
: null;
|
||||
? '→ ${message.to}'
|
||||
: null;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 3),
|
||||
@@ -451,10 +403,7 @@ class _ChatRow extends StatelessWidget {
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||
margin: const EdgeInsets.only(right: 5, top: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: senderColor.withAlpha(30),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
decoration: BoxDecoration(color: senderColor.withAlpha(30), borderRadius: BorderRadius.circular(2)),
|
||||
child: ClideText(message.from, fontSize: clideFontSmall, color: senderColor),
|
||||
),
|
||||
if (toLabel != null)
|
||||
@@ -480,13 +429,7 @@ class _ChatRow extends StatelessWidget {
|
||||
|
||||
/// Inline text input for the chat composer.
|
||||
class _ChatInputField extends StatelessWidget {
|
||||
const _ChatInputField({
|
||||
required this.controller,
|
||||
required this.focusNode,
|
||||
required this.tokens,
|
||||
required this.onSubmit,
|
||||
required this.placeholder,
|
||||
});
|
||||
const _ChatInputField({required this.controller, required this.focusNode, required this.tokens, required this.onSubmit, required this.placeholder});
|
||||
|
||||
final TextEditingController controller;
|
||||
final FocusNode focusNode;
|
||||
@@ -507,12 +450,7 @@ class _ChatInputField extends StatelessWidget {
|
||||
child: EditableText(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
style: TextStyle(
|
||||
fontFamily: 'JetBrains Mono',
|
||||
fontSize: clideFontSmall,
|
||||
color: tokens.globalForeground,
|
||||
height: 1.4,
|
||||
),
|
||||
style: TextStyle(fontFamily: 'JetBrains Mono', fontSize: clideFontSmall, color: tokens.globalForeground, height: 1.4),
|
||||
cursorColor: tokens.globalFocus,
|
||||
backgroundCursorColor: tokens.globalTextMuted,
|
||||
onSubmitted: onSubmit,
|
||||
|
||||
@@ -51,10 +51,7 @@ class _TeamPanelHostState extends State<TeamPanelHost> {
|
||||
void _onJoined(TeamMemberJoined m) {
|
||||
if (_controllers.containsKey(m.agentId)) return;
|
||||
final kernel = ClideKernel.of(context);
|
||||
_controllers[m.agentId] = ConversationController.fromBus(
|
||||
messages: kernel.messages,
|
||||
channel: ClaudeConversation.teammateChannel(m.agentId),
|
||||
);
|
||||
_controllers[m.agentId] = ConversationController.fromBus(messages: kernel.messages, channel: ClaudeConversation.teammateChannel(m.agentId));
|
||||
setState(() => _members.add(m));
|
||||
}
|
||||
|
||||
@@ -92,11 +89,7 @@ class _TeamPanelHostState extends State<TeamPanelHost> {
|
||||
}),
|
||||
),
|
||||
Expanded(
|
||||
child: _TeammateGrid(
|
||||
members: _members,
|
||||
controllers: _controllers,
|
||||
tokens: tokens,
|
||||
),
|
||||
child: _TeammateGrid(members: _members, controllers: _controllers, tokens: tokens),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -145,11 +138,7 @@ class _TeammateGrid extends StatelessWidget {
|
||||
children: [
|
||||
for (var r = 0; r < rows; r++)
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
for (var c = 0; c < cols; c++) Expanded(child: _cell(r * cols + c)),
|
||||
],
|
||||
),
|
||||
child: Row(children: [for (var c = 0; c < cols; c++) Expanded(child: _cell(r * cols + c))]),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -36,10 +36,13 @@ Future<bool> applyTicketPickUp(
|
||||
final id = data['id'] as String?;
|
||||
final status = data['status'] as String?;
|
||||
if (id != null && id.isNotEmpty && kPickUpStartableStatuses.contains(status)) {
|
||||
final resp = await ipc.request('pql.tickets.status', args: {
|
||||
'ids': [id],
|
||||
'status': 'in_progress',
|
||||
});
|
||||
final resp = await ipc.request(
|
||||
'pql.tickets.status',
|
||||
args: {
|
||||
'ids': [id],
|
||||
'status': 'in_progress',
|
||||
},
|
||||
);
|
||||
if (resp.ok) messages.publish('builtin.tickets', 'changed', {'id': id});
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -38,11 +38,11 @@ abstract final class ClaudeConversation {
|
||||
|
||||
/// Encode [status] for [agentId] as a [memberStatusChannel] message body.
|
||||
static Map<String, Object?> memberStatusData(String agentId, SessionStatus status) => {
|
||||
'agentId': agentId,
|
||||
if (status.model != null) 'model': status.model,
|
||||
if (status.permissionMode != null) 'permissionMode': status.permissionMode,
|
||||
if (status.contextTokens != null) 'contextTokens': status.contextTokens,
|
||||
};
|
||||
'agentId': agentId,
|
||||
if (status.model != null) 'model': status.model,
|
||||
if (status.permissionMode != null) 'permissionMode': status.permissionMode,
|
||||
if (status.contextTokens != null) 'contextTokens': status.contextTokens,
|
||||
};
|
||||
}
|
||||
|
||||
class TranscriptPublisher {
|
||||
@@ -50,16 +50,11 @@ class TranscriptPublisher {
|
||||
/// [ClaudeConversation.publisher] / [channel]. The subscription is
|
||||
/// attached synchronously, so a controller that subscribes before the
|
||||
/// reader's first poll never misses the initial tail.
|
||||
TranscriptPublisher({
|
||||
required MessageBus messages,
|
||||
required TranscriptReader reader,
|
||||
this.channel = ClaudeConversation.leadChannel,
|
||||
}) : _messages = messages,
|
||||
_reader = reader {
|
||||
TranscriptPublisher({required MessageBus messages, required TranscriptReader reader, this.channel = ClaudeConversation.leadChannel})
|
||||
: _messages = messages,
|
||||
_reader = reader {
|
||||
_sub = _reader.stream.listen((item) {
|
||||
_messages.publish(ClaudeConversation.publisher, channel, {
|
||||
ClaudeConversation.itemKey: item,
|
||||
});
|
||||
_messages.publish(ClaudeConversation.publisher, channel, {ClaudeConversation.itemKey: item});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -171,13 +171,7 @@ final class AssistantToolUse extends ConversationItem {
|
||||
/// driver has already resolved (workspace-relative paths are resolved before
|
||||
/// injection); [caption] is an optional one-line label.
|
||||
final class ImageMessage extends ConversationItem {
|
||||
const ImageMessage({
|
||||
required super.uuid,
|
||||
required super.timestamp,
|
||||
required super.isSidechain,
|
||||
required this.path,
|
||||
this.caption,
|
||||
});
|
||||
const ImageMessage({required super.uuid, required super.timestamp, required super.isSidechain, required this.path, this.caption});
|
||||
|
||||
/// Absolute path to the image file on disk.
|
||||
final String path;
|
||||
@@ -215,14 +209,7 @@ const _isolateParseThreshold = 64 * 1024;
|
||||
const _knownMajorVersions = {1, 2};
|
||||
|
||||
/// Record types to skip (do not emit as conversation items).
|
||||
const _skipTypes = {
|
||||
'attachment',
|
||||
'system',
|
||||
'last-prompt',
|
||||
'permission-mode',
|
||||
'file-history-snapshot',
|
||||
'queue-operation',
|
||||
};
|
||||
const _skipTypes = {'attachment', 'system', 'last-prompt', 'permission-mode', 'file-history-snapshot', 'queue-operation'};
|
||||
|
||||
/// Tails Claude Code's transcript JSONL and emits [ConversationItem]s.
|
||||
///
|
||||
@@ -242,11 +229,11 @@ class TranscriptReader {
|
||||
String? projectsBase,
|
||||
int? initialTailBytes,
|
||||
String? file,
|
||||
}) : _pollInterval = pollInterval,
|
||||
_onWarn = onWarn ?? _defaultWarn,
|
||||
_projectsBase = projectsBase ?? _defaultProjectsBase(),
|
||||
_initialTailBytes = initialTailBytes ?? _defaultInitialTailBytes,
|
||||
_explicitFile = file;
|
||||
}) : _pollInterval = pollInterval,
|
||||
_onWarn = onWarn ?? _defaultWarn,
|
||||
_projectsBase = projectsBase ?? _defaultProjectsBase(),
|
||||
_initialTailBytes = initialTailBytes ?? _defaultInitialTailBytes,
|
||||
_explicitFile = file;
|
||||
|
||||
final String workspacePath;
|
||||
final Duration _pollInterval;
|
||||
@@ -439,14 +426,7 @@ class TranscriptReader {
|
||||
/// (T-145, T-168). All fields nullable — a chunk only carries what it saw,
|
||||
/// and the reader [merge]s deltas into a running status.
|
||||
class SessionStatus {
|
||||
const SessionStatus({
|
||||
this.model,
|
||||
this.permissionMode,
|
||||
this.contextTokens,
|
||||
this.cost,
|
||||
this.contextWindow,
|
||||
this.rateLimitInfo,
|
||||
});
|
||||
const SessionStatus({this.model, this.permissionMode, this.contextTokens, this.cost, this.contextWindow, this.rateLimitInfo});
|
||||
|
||||
/// Assistant `message.model`, e.g. `claude-opus-4-7`.
|
||||
final String? model;
|
||||
@@ -475,13 +455,13 @@ class SessionStatus {
|
||||
|
||||
/// Overlay [other]'s non-null fields onto this one.
|
||||
SessionStatus merge(SessionStatus other) => SessionStatus(
|
||||
model: other.model ?? model,
|
||||
permissionMode: other.permissionMode ?? permissionMode,
|
||||
contextTokens: other.contextTokens ?? contextTokens,
|
||||
cost: other.cost ?? cost,
|
||||
contextWindow: other.contextWindow ?? contextWindow,
|
||||
rateLimitInfo: other.rateLimitInfo ?? rateLimitInfo,
|
||||
);
|
||||
model: other.model ?? model,
|
||||
permissionMode: other.permissionMode ?? permissionMode,
|
||||
contextTokens: other.contextTokens ?? contextTokens,
|
||||
cost: other.cost ?? cost,
|
||||
contextWindow: other.contextWindow ?? contextWindow,
|
||||
rateLimitInfo: other.rateLimitInfo ?? rateLimitInfo,
|
||||
);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
@@ -509,13 +489,13 @@ class _StatusAcc {
|
||||
int? contextWindow;
|
||||
String? rateLimitInfo;
|
||||
SessionStatus toStatus() => SessionStatus(
|
||||
model: model,
|
||||
permissionMode: permissionMode,
|
||||
contextTokens: contextTokens,
|
||||
cost: cost,
|
||||
contextWindow: contextWindow,
|
||||
rateLimitInfo: rateLimitInfo,
|
||||
);
|
||||
model: model,
|
||||
permissionMode: permissionMode,
|
||||
contextTokens: contextTokens,
|
||||
cost: cost,
|
||||
contextWindow: contextWindow,
|
||||
rateLimitInfo: rateLimitInfo,
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -550,8 +530,10 @@ void _parseLineInto(String line, List<ConversationItem> out, List<String> warnin
|
||||
final majorStr = dotIdx > 0 ? rawVersion.substring(0, dotIdx) : rawVersion;
|
||||
final major = int.tryParse(majorStr);
|
||||
if (major != null && !_knownMajorVersions.contains(major)) {
|
||||
warnings.add('unfamiliar transcript version "$rawVersion" (major=$major); '
|
||||
'parsing will degrade gracefully');
|
||||
warnings.add(
|
||||
'unfamiliar transcript version "$rawVersion" (major=$major); '
|
||||
'parsing will degrade gracefully',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,14 +610,17 @@ void _parseUserInto(
|
||||
|
||||
if (content is String) {
|
||||
if (content.isNotEmpty) {
|
||||
out.add(UserMessage(
|
||||
out.add(
|
||||
UserMessage(
|
||||
uuid: uuid,
|
||||
timestamp: timestamp,
|
||||
isSidechain: isSidechain,
|
||||
parentUuid: parentUuid,
|
||||
parentToolUseId: parentToolUseId,
|
||||
text: content,
|
||||
injected: injected));
|
||||
injected: injected,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -650,16 +635,18 @@ void _parseUserInto(
|
||||
if (text.isNotEmpty) textParts.add(text);
|
||||
case 'tool_result':
|
||||
final rawContent = item['content'];
|
||||
out.add(ToolResultMessage(
|
||||
uuid: uuid,
|
||||
timestamp: timestamp,
|
||||
isSidechain: isSidechain,
|
||||
parentUuid: parentUuid,
|
||||
parentToolUseId: parentToolUseId,
|
||||
toolUseId: item['tool_use_id'] as String? ?? '',
|
||||
content: rawContent is String ? rawContent : jsonEncode(rawContent),
|
||||
isError: item['is_error'] as bool? ?? false,
|
||||
));
|
||||
out.add(
|
||||
ToolResultMessage(
|
||||
uuid: uuid,
|
||||
timestamp: timestamp,
|
||||
isSidechain: isSidechain,
|
||||
parentUuid: parentUuid,
|
||||
parentToolUseId: parentToolUseId,
|
||||
toolUseId: item['tool_use_id'] as String? ?? '',
|
||||
content: rawContent is String ? rawContent : jsonEncode(rawContent),
|
||||
isError: item['is_error'] as bool? ?? false,
|
||||
),
|
||||
);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -689,27 +676,45 @@ void _parseAssistantInto(
|
||||
case 'text':
|
||||
final text = item['text'] as String? ?? '';
|
||||
if (text.isNotEmpty) {
|
||||
out.add(AssistantTextMessage(
|
||||
uuid: uuid, timestamp: timestamp, isSidechain: isSidechain, parentUuid: parentUuid, parentToolUseId: parentToolUseId, text: text));
|
||||
out.add(
|
||||
AssistantTextMessage(
|
||||
uuid: uuid,
|
||||
timestamp: timestamp,
|
||||
isSidechain: isSidechain,
|
||||
parentUuid: parentUuid,
|
||||
parentToolUseId: parentToolUseId,
|
||||
text: text,
|
||||
),
|
||||
);
|
||||
}
|
||||
case 'thinking':
|
||||
final thinking = item['thinking'] as String? ?? '';
|
||||
if (thinking.isNotEmpty) {
|
||||
out.add(AssistantThinkingMessage(
|
||||
uuid: uuid, timestamp: timestamp, isSidechain: isSidechain, parentUuid: parentUuid, parentToolUseId: parentToolUseId, thinking: thinking));
|
||||
out.add(
|
||||
AssistantThinkingMessage(
|
||||
uuid: uuid,
|
||||
timestamp: timestamp,
|
||||
isSidechain: isSidechain,
|
||||
parentUuid: parentUuid,
|
||||
parentToolUseId: parentToolUseId,
|
||||
thinking: thinking,
|
||||
),
|
||||
);
|
||||
}
|
||||
case 'tool_use':
|
||||
final rawInput = item['input'];
|
||||
out.add(AssistantToolUse(
|
||||
uuid: uuid,
|
||||
timestamp: timestamp,
|
||||
isSidechain: isSidechain,
|
||||
parentUuid: parentUuid,
|
||||
parentToolUseId: parentToolUseId,
|
||||
toolUseId: item['id'] as String? ?? '',
|
||||
name: item['name'] as String? ?? '',
|
||||
input: rawInput is Map ? rawInput.cast<String, dynamic>() : <String, dynamic>{},
|
||||
));
|
||||
out.add(
|
||||
AssistantToolUse(
|
||||
uuid: uuid,
|
||||
timestamp: timestamp,
|
||||
isSidechain: isSidechain,
|
||||
parentUuid: parentUuid,
|
||||
parentToolUseId: parentToolUseId,
|
||||
toolUseId: item['id'] as String? ?? '',
|
||||
name: item['name'] as String? ?? '',
|
||||
input: rawInput is Map ? rawInput.cast<String, dynamic>() : <String, dynamic>{},
|
||||
),
|
||||
);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user