mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-10 11:23:32 +08:00
feat(ecc2): persist file activity diff previews
This commit is contained in:
@@ -134,6 +134,7 @@ pub struct FileActivityEntry {
|
|||||||
pub action: FileActivityAction,
|
pub action: FileActivityAction,
|
||||||
pub path: String,
|
pub path: String,
|
||||||
pub summary: String,
|
pub summary: String,
|
||||||
|
pub diff_preview: Option<String>,
|
||||||
pub timestamp: DateTime<Utc>,
|
pub timestamp: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -759,6 +759,8 @@ impl StateStore {
|
|||||||
struct ToolActivityFileEvent {
|
struct ToolActivityFileEvent {
|
||||||
path: String,
|
path: String,
|
||||||
action: String,
|
action: String,
|
||||||
|
#[serde(default)]
|
||||||
|
diff_preview: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
let file = File::open(metrics_path)
|
let file = File::open(metrics_path)
|
||||||
@@ -800,6 +802,7 @@ impl StateStore {
|
|||||||
.map(|path| PersistedFileEvent {
|
.map(|path| PersistedFileEvent {
|
||||||
path,
|
path,
|
||||||
action: infer_file_activity_action(&row.tool_name),
|
action: infer_file_activity_action(&row.tool_name),
|
||||||
|
diff_preview: None,
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
@@ -814,6 +817,7 @@ impl StateStore {
|
|||||||
path,
|
path,
|
||||||
action: parse_file_activity_action(&event.action)
|
action: parse_file_activity_action(&event.action)
|
||||||
.unwrap_or_else(|| infer_file_activity_action(&row.tool_name)),
|
.unwrap_or_else(|| infer_file_activity_action(&row.tool_name)),
|
||||||
|
diff_preview: normalize_optional_string(event.diff_preview),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
@@ -1594,6 +1598,7 @@ impl StateStore {
|
|||||||
Some(PersistedFileEvent {
|
Some(PersistedFileEvent {
|
||||||
path,
|
path,
|
||||||
action: infer_file_activity_action(&tool_name),
|
action: infer_file_activity_action(&tool_name),
|
||||||
|
diff_preview: None,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
@@ -1605,6 +1610,7 @@ impl StateStore {
|
|||||||
action: event.action,
|
action: event.action,
|
||||||
path: event.path,
|
path: event.path,
|
||||||
summary: summary.clone(),
|
summary: summary.clone(),
|
||||||
|
diff_preview: event.diff_preview,
|
||||||
timestamp: occurred_at,
|
timestamp: occurred_at,
|
||||||
});
|
});
|
||||||
if events.len() >= limit {
|
if events.len() >= limit {
|
||||||
@@ -1621,6 +1627,8 @@ impl StateStore {
|
|||||||
struct PersistedFileEvent {
|
struct PersistedFileEvent {
|
||||||
path: String,
|
path: String,
|
||||||
action: FileActivityAction,
|
action: FileActivityAction,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
diff_preview: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_persisted_file_events(value: &str) -> Option<Vec<PersistedFileEvent>> {
|
fn parse_persisted_file_events(value: &str) -> Option<Vec<PersistedFileEvent>> {
|
||||||
@@ -1635,6 +1643,7 @@ fn parse_persisted_file_events(value: &str) -> Option<Vec<PersistedFileEvent>> {
|
|||||||
Some(PersistedFileEvent {
|
Some(PersistedFileEvent {
|
||||||
path,
|
path,
|
||||||
action: event.action,
|
action: event.action,
|
||||||
|
diff_preview: normalize_optional_string(event.diff_preview),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -1656,6 +1665,17 @@ fn parse_file_activity_action(value: &str) -> Option<FileActivityAction> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn normalize_optional_string(value: Option<String>) -> Option<String> {
|
||||||
|
value.and_then(|value| {
|
||||||
|
let trimmed = value.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(trimmed.to_string())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn infer_file_activity_action(tool_name: &str) -> FileActivityAction {
|
fn infer_file_activity_action(tool_name: &str) -> FileActivityAction {
|
||||||
let tool_name = tool_name.trim().to_ascii_lowercase();
|
let tool_name = tool_name.trim().to_ascii_lowercase();
|
||||||
if tool_name.contains("read") {
|
if tool_name.contains("read") {
|
||||||
@@ -1938,6 +1958,50 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_file_activity_preserves_diff_previews() -> Result<()> {
|
||||||
|
let tempdir = TestDir::new("store-file-activity-diffs")?;
|
||||||
|
let db = StateStore::open(&tempdir.path().join("state.db"))?;
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
db.insert_session(&Session {
|
||||||
|
id: "session-1".to_string(),
|
||||||
|
task: "sync tools".to_string(),
|
||||||
|
agent_type: "claude".to_string(),
|
||||||
|
working_dir: PathBuf::from("/tmp"),
|
||||||
|
state: SessionState::Running,
|
||||||
|
pid: None,
|
||||||
|
worktree: None,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
last_heartbeat_at: now,
|
||||||
|
metrics: SessionMetrics::default(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let metrics_dir = tempdir.path().join("metrics");
|
||||||
|
fs::create_dir_all(&metrics_dir)?;
|
||||||
|
let metrics_path = metrics_dir.join("tool-usage.jsonl");
|
||||||
|
fs::write(
|
||||||
|
&metrics_path,
|
||||||
|
concat!(
|
||||||
|
"{\"id\":\"evt-1\",\"session_id\":\"session-1\",\"tool_name\":\"Edit\",\"input_summary\":\"Edit src/config.ts\",\"output_summary\":\"updated config\",\"file_paths\":[\"src/config.ts\"],\"file_events\":[{\"path\":\"src/config.ts\",\"action\":\"modify\",\"diff_preview\":\"API_URL=http://localhost:3000 -> API_URL=https://api.example.com\"}],\"timestamp\":\"2026-04-09T00:00:00Z\"}\n"
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
db.sync_tool_activity_metrics(&metrics_path)?;
|
||||||
|
|
||||||
|
let activity = db.list_file_activity("session-1", 10)?;
|
||||||
|
assert_eq!(activity.len(), 1);
|
||||||
|
assert_eq!(activity[0].action, FileActivityAction::Modify);
|
||||||
|
assert_eq!(activity[0].path, "src/config.ts");
|
||||||
|
assert_eq!(
|
||||||
|
activity[0].diff_preview.as_deref(),
|
||||||
|
Some("API_URL=http://localhost:3000 -> API_URL=https://api.example.com")
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn refresh_session_durations_updates_running_and_terminal_sessions() -> Result<()> {
|
fn refresh_session_durations_updates_running_and_terminal_sessions() -> Result<()> {
|
||||||
let tempdir = TestDir::new("store-duration-metrics")?;
|
let tempdir = TestDir::new("store-duration-metrics")?;
|
||||||
|
|||||||
@@ -5398,11 +5398,18 @@ fn session_state_color(state: &SessionState) -> Color {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn file_activity_summary(entry: &FileActivityEntry) -> String {
|
fn file_activity_summary(entry: &FileActivityEntry) -> String {
|
||||||
format!(
|
let mut summary = format!(
|
||||||
"{} {}",
|
"{} {}",
|
||||||
file_activity_verb(entry.action.clone()),
|
file_activity_verb(entry.action.clone()),
|
||||||
truncate_for_dashboard(&entry.path, 72)
|
truncate_for_dashboard(&entry.path, 72)
|
||||||
)
|
);
|
||||||
|
|
||||||
|
if let Some(diff_preview) = entry.diff_preview.as_ref() {
|
||||||
|
summary.push_str(" | ");
|
||||||
|
summary.push_str(&truncate_for_dashboard(diff_preview, 56));
|
||||||
|
}
|
||||||
|
|
||||||
|
summary
|
||||||
}
|
}
|
||||||
|
|
||||||
fn file_activity_verb(action: crate::session::FileActivityAction) -> &'static str {
|
fn file_activity_verb(action: crate::session::FileActivityAction) -> &'static str {
|
||||||
@@ -6085,7 +6092,7 @@ mod tests {
|
|||||||
&metrics_path,
|
&metrics_path,
|
||||||
concat!(
|
concat!(
|
||||||
"{\"id\":\"evt-1\",\"session_id\":\"focus-12345678\",\"tool_name\":\"Read\",\"input_summary\":\"Read src/lib.rs\",\"output_summary\":\"ok\",\"file_paths\":[\"src/lib.rs\"],\"timestamp\":\"2026-04-09T00:00:00Z\"}\n",
|
"{\"id\":\"evt-1\",\"session_id\":\"focus-12345678\",\"tool_name\":\"Read\",\"input_summary\":\"Read src/lib.rs\",\"output_summary\":\"ok\",\"file_paths\":[\"src/lib.rs\"],\"timestamp\":\"2026-04-09T00:00:00Z\"}\n",
|
||||||
"{\"id\":\"evt-2\",\"session_id\":\"focus-12345678\",\"tool_name\":\"Write\",\"input_summary\":\"Write README.md\",\"output_summary\":\"updated readme\",\"file_paths\":[\"README.md\"],\"timestamp\":\"2026-04-09T00:01:00Z\"}\n"
|
"{\"id\":\"evt-2\",\"session_id\":\"focus-12345678\",\"tool_name\":\"Write\",\"input_summary\":\"Write README.md\",\"output_summary\":\"updated readme\",\"file_paths\":[\"README.md\"],\"file_events\":[{\"path\":\"README.md\",\"action\":\"create\",\"diff_preview\":\"+ # ECC 2.0\"}],\"timestamp\":\"2026-04-09T00:01:00Z\"}\n"
|
||||||
),
|
),
|
||||||
)?;
|
)?;
|
||||||
dashboard.db.sync_tool_activity_metrics(&metrics_path)?;
|
dashboard.db.sync_tool_activity_metrics(&metrics_path)?;
|
||||||
@@ -6095,11 +6102,13 @@ mod tests {
|
|||||||
let rendered = dashboard.rendered_output_text(180, 30);
|
let rendered = dashboard.rendered_output_text(180, 30);
|
||||||
assert!(rendered.contains("read src/lib.rs"));
|
assert!(rendered.contains("read src/lib.rs"));
|
||||||
assert!(rendered.contains("create README.md"));
|
assert!(rendered.contains("create README.md"));
|
||||||
|
assert!(rendered.contains("+ # ECC 2.0"));
|
||||||
assert!(!rendered.contains("files touched 2"));
|
assert!(!rendered.contains("files touched 2"));
|
||||||
|
|
||||||
let metrics_text = dashboard.selected_session_metrics_text();
|
let metrics_text = dashboard.selected_session_metrics_text();
|
||||||
assert!(metrics_text.contains("Recent file activity"));
|
assert!(metrics_text.contains("Recent file activity"));
|
||||||
assert!(metrics_text.contains("create README.md"));
|
assert!(metrics_text.contains("create README.md"));
|
||||||
|
assert!(metrics_text.contains("+ # ECC 2.0"));
|
||||||
assert!(metrics_text.contains("read src/lib.rs"));
|
assert!(metrics_text.contains("read src/lib.rs"));
|
||||||
|
|
||||||
let _ = fs::remove_dir_all(root);
|
let _ = fs::remove_dir_all(root);
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ function pushPathCandidate(paths, value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function pushFileEvent(events, value, action) {
|
function pushFileEvent(events, value, action, diffPreview) {
|
||||||
const candidate = String(value || '').trim();
|
const candidate = String(value || '').trim();
|
||||||
if (!candidate) {
|
if (!candidate) {
|
||||||
return;
|
return;
|
||||||
@@ -70,11 +70,52 @@ function pushFileEvent(events, value, action) {
|
|||||||
if (/^(https?:\/\/|app:\/\/|plugin:\/\/|mcp:\/\/)/i.test(candidate)) {
|
if (/^(https?:\/\/|app:\/\/|plugin:\/\/|mcp:\/\/)/i.test(candidate)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!events.some(event => event.path === candidate && event.action === action)) {
|
const normalizedDiffPreview = typeof diffPreview === 'string' && diffPreview.trim()
|
||||||
events.push({ path: candidate, action });
|
? diffPreview.trim()
|
||||||
|
: undefined;
|
||||||
|
if (!events.some(event =>
|
||||||
|
event.path === candidate
|
||||||
|
&& event.action === action
|
||||||
|
&& (event.diff_preview || undefined) === normalizedDiffPreview
|
||||||
|
)) {
|
||||||
|
const event = { path: candidate, action };
|
||||||
|
if (normalizedDiffPreview) {
|
||||||
|
event.diff_preview = normalizedDiffPreview;
|
||||||
|
}
|
||||||
|
events.push(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeDiffText(value, maxLength = 96) {
|
||||||
|
if (typeof value !== 'string' || !value.trim()) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return truncateSummary(value, maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildReplacementPreview(oldValue, newValue) {
|
||||||
|
const before = sanitizeDiffText(oldValue);
|
||||||
|
const after = sanitizeDiffText(newValue);
|
||||||
|
if (!before && !after) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (!before) {
|
||||||
|
return `-> ${after}`;
|
||||||
|
}
|
||||||
|
if (!after) {
|
||||||
|
return `${before} ->`;
|
||||||
|
}
|
||||||
|
return `${before} -> ${after}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCreationPreview(content) {
|
||||||
|
const normalized = sanitizeDiffText(content);
|
||||||
|
if (!normalized) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return `+ ${normalized}`;
|
||||||
|
}
|
||||||
|
|
||||||
function inferDefaultFileAction(toolName) {
|
function inferDefaultFileAction(toolName) {
|
||||||
const normalized = String(toolName || '').trim().toLowerCase();
|
const normalized = String(toolName || '').trim().toLowerCase();
|
||||||
if (normalized.includes('read')) {
|
if (normalized.includes('read')) {
|
||||||
@@ -129,6 +170,11 @@ function collectFilePaths(value, paths) {
|
|||||||
for (const [key, nested] of Object.entries(value)) {
|
for (const [key, nested] of Object.entries(value)) {
|
||||||
if (FILE_PATH_KEYS.has(key)) {
|
if (FILE_PATH_KEYS.has(key)) {
|
||||||
collectFilePaths(nested, paths);
|
collectFilePaths(nested, paths);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nested && (Array.isArray(nested) || typeof nested === 'object')) {
|
||||||
|
collectFilePaths(nested, paths);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,20 +188,39 @@ function extractFilePaths(toolInput) {
|
|||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectFileEvents(toolName, value, events, key = null) {
|
function fileEventDiffPreview(toolName, value, action) {
|
||||||
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value.old_string === 'string' || typeof value.new_string === 'string') {
|
||||||
|
return buildReplacementPreview(value.old_string, value.new_string);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'create') {
|
||||||
|
return buildCreationPreview(value.content || value.file_text || value.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectFileEvents(toolName, value, events, key = null, parentValue = null) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
for (const entry of value) {
|
for (const entry of value) {
|
||||||
collectFileEvents(toolName, entry, events, key);
|
collectFileEvents(toolName, entry, events, key, parentValue);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
pushFileEvent(events, value, actionForFileKey(toolName, key));
|
if (key && FILE_PATH_KEYS.has(key)) {
|
||||||
|
const action = actionForFileKey(toolName, key);
|
||||||
|
pushFileEvent(events, value, action, fileEventDiffPreview(toolName, parentValue, action));
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +230,12 @@ function collectFileEvents(toolName, value, events, key = null) {
|
|||||||
|
|
||||||
for (const [nestedKey, nested] of Object.entries(value)) {
|
for (const [nestedKey, nested] of Object.entries(value)) {
|
||||||
if (FILE_PATH_KEYS.has(nestedKey)) {
|
if (FILE_PATH_KEYS.has(nestedKey)) {
|
||||||
collectFileEvents(toolName, nested, events, nestedKey);
|
collectFileEvents(toolName, nested, events, nestedKey, value);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nested && (Array.isArray(nested) || typeof nested === 'object')) {
|
||||||
|
collectFileEvents(toolName, nested, events, null, nested);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,8 +304,10 @@ function buildActivityRow(input, env = process.env) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const toolInput = input?.tool_input || {};
|
const toolInput = input?.tool_input || {};
|
||||||
const filePaths = extractFilePaths(toolInput);
|
|
||||||
const fileEvents = extractFileEvents(toolName, toolInput);
|
const fileEvents = extractFileEvents(toolName, toolInput);
|
||||||
|
const filePaths = fileEvents.length > 0
|
||||||
|
? [...new Set(fileEvents.map(event => event.path))]
|
||||||
|
: extractFilePaths(toolInput);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `tool-${Date.now()}-${crypto.randomBytes(6).toString('hex')}`,
|
id: `tool-${Date.now()}-${crypto.randomBytes(6).toString('hex')}`,
|
||||||
|
|||||||
@@ -130,6 +130,83 @@ function runTests() {
|
|||||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||||
}) ? passed++ : failed++);
|
}) ? passed++ : failed++);
|
||||||
|
|
||||||
|
(test('captures replacement diff previews for edit tool input', () => {
|
||||||
|
const tmpHome = makeTempDir();
|
||||||
|
const input = {
|
||||||
|
tool_name: 'Edit',
|
||||||
|
tool_input: {
|
||||||
|
file_path: 'src/config.ts',
|
||||||
|
old_string: 'API_URL=http://localhost:3000',
|
||||||
|
new_string: 'API_URL=https://api.example.com',
|
||||||
|
},
|
||||||
|
tool_output: { output: 'updated config' },
|
||||||
|
};
|
||||||
|
const result = runScript(input, {
|
||||||
|
...withTempHome(tmpHome),
|
||||||
|
CLAUDE_HOOK_EVENT_NAME: 'PostToolUse',
|
||||||
|
ECC_SESSION_ID: 'ecc-session-edit',
|
||||||
|
});
|
||||||
|
assert.strictEqual(result.code, 0);
|
||||||
|
|
||||||
|
const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'tool-usage.jsonl');
|
||||||
|
const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim());
|
||||||
|
assert.deepStrictEqual(row.file_events, [
|
||||||
|
{
|
||||||
|
path: 'src/config.ts',
|
||||||
|
action: 'modify',
|
||||||
|
diff_preview: 'API_URL=http://localhost:3000 -> API_URL=https://api.example.com',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||||
|
}) ? passed++ : failed++);
|
||||||
|
|
||||||
|
(test('captures MultiEdit nested edits with typed diff previews', () => {
|
||||||
|
const tmpHome = makeTempDir();
|
||||||
|
const input = {
|
||||||
|
tool_name: 'MultiEdit',
|
||||||
|
tool_input: {
|
||||||
|
edits: [
|
||||||
|
{
|
||||||
|
file_path: 'src/a.ts',
|
||||||
|
old_string: 'const a = 1;',
|
||||||
|
new_string: 'const a = 2;',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file_path: 'src/b.ts',
|
||||||
|
old_string: 'old name',
|
||||||
|
new_string: 'new name',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
tool_output: { output: 'updated two files' },
|
||||||
|
};
|
||||||
|
const result = runScript(input, {
|
||||||
|
...withTempHome(tmpHome),
|
||||||
|
CLAUDE_HOOK_EVENT_NAME: 'PostToolUse',
|
||||||
|
ECC_SESSION_ID: 'ecc-session-multiedit',
|
||||||
|
});
|
||||||
|
assert.strictEqual(result.code, 0);
|
||||||
|
|
||||||
|
const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'tool-usage.jsonl');
|
||||||
|
const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim());
|
||||||
|
assert.deepStrictEqual(row.file_paths, ['src/a.ts', 'src/b.ts']);
|
||||||
|
assert.deepStrictEqual(row.file_events, [
|
||||||
|
{
|
||||||
|
path: 'src/a.ts',
|
||||||
|
action: 'modify',
|
||||||
|
diff_preview: 'const a = 1; -> const a = 2;',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'src/b.ts',
|
||||||
|
action: 'modify',
|
||||||
|
diff_preview: 'old name -> new name',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||||
|
}) ? passed++ : failed++);
|
||||||
|
|
||||||
(test('prefers ECC_SESSION_ID over CLAUDE_SESSION_ID and redacts bash summaries', () => {
|
(test('prefers ECC_SESSION_ID over CLAUDE_SESSION_ID and redacts bash summaries', () => {
|
||||||
const tmpHome = makeTempDir();
|
const tmpHome = makeTempDir();
|
||||||
const input = {
|
const input = {
|
||||||
|
|||||||
Reference in New Issue
Block a user