claude-ps 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -56,7 +56,7 @@ async function getClaudeProcesses() {
56
56
  let stdout;
57
57
  try {
58
58
  const result = await execAsync(
59
- `ps -eo pid,tty,command | grep -E '^\\s*[0-9]+\\s+\\S+\\s+claude\\s*$' | grep -v grep`
59
+ `ps -eo pid,tty,command | grep -E '^\\s*[0-9]+\\s+\\S+\\s+claude(\\s|$)' | grep -v 'chrome-native-host' | grep -v grep`
60
60
  );
61
61
  stdout = result.stdout;
62
62
  } catch {
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  getClaudeProcesses,
4
4
  killProcess
5
- } from "./chunk-TPK4XYR3.js";
5
+ } from "./chunk-EZHIVMX4.js";
6
6
 
7
7
  // src/index.tsx
8
8
  import { withFullScreen } from "fullscreen-ink";
@@ -193,7 +193,7 @@ function formatElapsed(elapsed) {
193
193
 
194
194
  // src/components/ui/DetailPanel.tsx
195
195
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
196
- function DetailPanel({ process: proc }) {
196
+ function DetailPanel({ process: proc, isLive = true }) {
197
197
  if (!proc) {
198
198
  return /* @__PURE__ */ jsx4(EmptyPrompt, { message: "\u9009\u62E9\u4E00\u4E2A\u8FDB\u7A0B\u67E5\u770B\u8BE6\u60C5" });
199
199
  }
@@ -208,7 +208,10 @@ function DetailPanel({ process: proc }) {
208
208
  /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: "Session:" }),
209
209
  /* @__PURE__ */ jsx4(Text4, { color: COLORS.label, wrap: "truncate", children: proc.sessionPath || "\u65E0\u4F1A\u8BDD\u6587\u4EF6" })
210
210
  ] }),
211
- /* @__PURE__ */ jsx4(Box4, { paddingBottom: 1, children: /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: "\u6700\u8FD1\u5BF9\u8BDD" }) }),
211
+ /* @__PURE__ */ jsxs4(Box4, { paddingBottom: 1, flexDirection: "row", gap: 1, children: [
212
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: "\u6700\u8FD1\u5BF9\u8BDD" }),
213
+ isLive && proc.sessionPath && /* @__PURE__ */ jsx4(Text4, { color: "green", children: "\u25CF \u5B9E\u65F6" })
214
+ ] }),
212
215
  /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", children: proc.messages.length === 0 ? /* @__PURE__ */ jsx4(Text4, { color: COLORS.label, children: "\u65E0\u5BF9\u8BDD\u8BB0\u5F55" }) : proc.messages.map((msg) => /* @__PURE__ */ jsxs4(Box4, { flexDirection: "row", children: [
213
216
  /* @__PURE__ */ jsxs4(Text4, { color: msg.role === "user" ? COLORS.current : "blue", children: [
214
217
  msg.role === "user" ? "[User]" : "[Claude]",
@@ -297,7 +300,7 @@ function ProcessList({
297
300
  }
298
301
 
299
302
  // src/hooks/useProcesses.ts
300
- import { useCallback, useEffect, useMemo, useState } from "react";
303
+ import { useCallback, useEffect as useEffect2, useMemo, useRef as useRef2, useState } from "react";
301
304
 
302
305
  // src/utils/session.ts
303
306
  import { readFile, readdir } from "fs/promises";
@@ -353,7 +356,45 @@ async function getSessionPath(cwd, startTime) {
353
356
  return "";
354
357
  }
355
358
  }
356
- async function getRecentMessages(sessionPath, limit = 5) {
359
+ async function getNewMessages(sessionPath, fromLine) {
360
+ if (!sessionPath) return { messages: [], totalLines: 0 };
361
+ try {
362
+ const content = await readFile(sessionPath, "utf-8");
363
+ const lines = content.trim().split("\n");
364
+ const totalLines = lines.length;
365
+ const newLines = lines.slice(fromLine);
366
+ const messages = [];
367
+ for (const line of newLines) {
368
+ try {
369
+ const entry = JSON.parse(line);
370
+ if (entry.type === "user" && entry.message?.content) {
371
+ const text = extractUserText(entry.message.content);
372
+ if (text && !isMetaMessage(text)) {
373
+ messages.push({
374
+ role: "user",
375
+ content: truncate(text, 100),
376
+ timestamp: entry.timestamp || ""
377
+ });
378
+ }
379
+ } else if (entry.type === "assistant" && entry.message?.content) {
380
+ const text = extractAssistantText(entry.message.content);
381
+ if (text) {
382
+ messages.push({
383
+ role: "assistant",
384
+ content: truncate(text, 100),
385
+ timestamp: entry.timestamp || ""
386
+ });
387
+ }
388
+ }
389
+ } catch {
390
+ }
391
+ }
392
+ return { messages, totalLines };
393
+ } catch {
394
+ return { messages: [], totalLines: 0 };
395
+ }
396
+ }
397
+ async function getAllMessages(sessionPath) {
357
398
  if (!sessionPath) return [];
358
399
  try {
359
400
  const content = await readFile(sessionPath, "utf-8");
@@ -384,7 +425,7 @@ async function getRecentMessages(sessionPath, limit = 5) {
384
425
  } catch {
385
426
  }
386
427
  }
387
- return messages.slice(-limit);
428
+ return messages;
388
429
  } catch {
389
430
  return [];
390
431
  }
@@ -409,6 +450,50 @@ function truncate(text, maxLen) {
409
450
  return `${clean.slice(0, maxLen - 3)}...`;
410
451
  }
411
452
 
453
+ // src/hooks/useSessionWatcher.ts
454
+ import { useEffect, useRef } from "react";
455
+ import chokidar from "chokidar";
456
+ function useSessionWatcher(sessionPaths, onFileChange) {
457
+ const watcherRef = useRef(null);
458
+ const debounceTimersRef = useRef(/* @__PURE__ */ new Map());
459
+ useEffect(() => {
460
+ const validPaths = sessionPaths.filter((p) => p && p.length > 0);
461
+ if (validPaths.length === 0) return;
462
+ const watcher = chokidar.watch(validPaths, {
463
+ persistent: true,
464
+ ignoreInitial: true,
465
+ // 忽略初始添加事件
466
+ awaitWriteFinish: {
467
+ stabilityThreshold: 50,
468
+ // 文件稳定 50ms 后触发
469
+ pollInterval: 25
470
+ }
471
+ });
472
+ watcher.on("change", (path) => {
473
+ const existingTimer = debounceTimersRef.current.get(path);
474
+ if (existingTimer) {
475
+ clearTimeout(existingTimer);
476
+ }
477
+ const timer = setTimeout(() => {
478
+ onFileChange(path);
479
+ debounceTimersRef.current.delete(path);
480
+ }, 100);
481
+ debounceTimersRef.current.set(path, timer);
482
+ });
483
+ watcherRef.current = watcher;
484
+ return () => {
485
+ for (const timer of debounceTimersRef.current.values()) {
486
+ clearTimeout(timer);
487
+ }
488
+ debounceTimersRef.current.clear();
489
+ if (watcherRef.current) {
490
+ watcherRef.current.close();
491
+ watcherRef.current = null;
492
+ }
493
+ };
494
+ }, [sessionPaths, onFileChange]);
495
+ }
496
+
412
497
  // src/hooks/useProcesses.ts
413
498
  function sortProcesses(processes, sortField) {
414
499
  const sorted = [...processes];
@@ -433,13 +518,21 @@ function useProcesses(interval2) {
433
518
  const [error, setError] = useState(null);
434
519
  const [selectedIndex, setSelectedIndex] = useState(0);
435
520
  const [sortField, setSortField] = useState("default");
521
+ const sessionLineNumbers = useRef2(/* @__PURE__ */ new Map());
436
522
  const refresh = useCallback(async () => {
437
523
  try {
438
524
  const procs = await getClaudeProcesses();
439
525
  const enriched = await Promise.all(
440
526
  procs.map(async (proc) => {
441
527
  const sessionPath = await getSessionPath(proc.cwd, proc.startTime);
442
- const messages = await getRecentMessages(sessionPath);
528
+ const messages = await getAllMessages(sessionPath);
529
+ if (sessionPath && !sessionLineNumbers.current.has(sessionPath)) {
530
+ const content = await import("fs/promises").then(
531
+ (m) => m.readFile(sessionPath, "utf-8").catch(() => "")
532
+ );
533
+ const lines = content.trim().split("\n");
534
+ sessionLineNumbers.current.set(sessionPath, lines.length);
535
+ }
443
536
  return {
444
537
  ...proc,
445
538
  sessionPath,
@@ -458,7 +551,37 @@ function useProcesses(interval2) {
458
551
  setLoading(false);
459
552
  }
460
553
  }, []);
461
- useEffect(() => {
554
+ const updateSessionMessages = useCallback(async (sessionPath) => {
555
+ if (!sessionPath) return;
556
+ try {
557
+ const fromLine = sessionLineNumbers.current.get(sessionPath) || 0;
558
+ const { messages: newMessages, totalLines } = await getNewMessages(
559
+ sessionPath,
560
+ fromLine
561
+ );
562
+ if (newMessages.length > 0) {
563
+ setRawProcesses(
564
+ (prev) => prev.map((proc) => {
565
+ if (proc.sessionPath === sessionPath) {
566
+ return {
567
+ ...proc,
568
+ messages: [...proc.messages, ...newMessages]
569
+ };
570
+ }
571
+ return proc;
572
+ })
573
+ );
574
+ sessionLineNumbers.current.set(sessionPath, totalLines);
575
+ }
576
+ } catch {
577
+ }
578
+ }, []);
579
+ const sessionPaths = useMemo(
580
+ () => rawProcesses.map((p) => p.sessionPath).filter((p) => p),
581
+ [rawProcesses]
582
+ );
583
+ useSessionWatcher(sessionPaths, updateSessionMessages);
584
+ useEffect2(() => {
462
585
  refresh();
463
586
  const timer = setInterval(refresh, interval2 * 1e3);
464
587
  return () => clearInterval(timer);
@@ -590,7 +713,7 @@ function App({ interval: interval2 }) {
590
713
  height: contentHeight,
591
714
  paddingX: 4,
592
715
  paddingY: 2,
593
- children: /* @__PURE__ */ jsx7(DetailPanel, { process: selectedProcess })
716
+ children: /* @__PURE__ */ jsx7(DetailPanel, { process: selectedProcess, isLive: true })
594
717
  }
595
718
  )
596
719
  ] }),
@@ -670,7 +793,7 @@ var cli = meow(
670
793
  );
671
794
  var { list, json, interval } = cli.flags;
672
795
  if (list || json) {
673
- const { getClaudeProcesses: getClaudeProcesses2 } = await import("./process-2SZ7S64S.js");
796
+ const { getClaudeProcesses: getClaudeProcesses2 } = await import("./process-AUO5UVTV.js");
674
797
  const processes = await getClaudeProcesses2();
675
798
  if (json) {
676
799
  console.log(JSON.stringify(processes, null, 2));
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  getClaudeProcesses,
4
4
  killProcess
5
- } from "./chunk-TPK4XYR3.js";
5
+ } from "./chunk-EZHIVMX4.js";
6
6
  export {
7
7
  getClaudeProcesses,
8
8
  killProcess
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "claude-ps",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "TUI application for viewing and managing Claude Code processes",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
8
  "claude-ps": "dist/index.js"
9
9
  },
10
- "files": ["dist"],
10
+ "files": [
11
+ "dist"
12
+ ],
11
13
  "scripts": {
12
14
  "dev": "tsx src/index.tsx",
13
15
  "build": "tsup",
@@ -17,11 +19,17 @@
17
19
  "typecheck": "tsc --noEmit",
18
20
  "all": "pnpm format && pnpm typecheck && pnpm lint"
19
21
  },
20
- "keywords": ["claude", "tui", "terminal", "process-manager"],
22
+ "keywords": [
23
+ "claude",
24
+ "tui",
25
+ "terminal",
26
+ "process-manager"
27
+ ],
21
28
  "author": "ziheng",
22
29
  "license": "MIT",
23
30
  "dependencies": {
24
31
  "chalk": "^5.3.0",
32
+ "chokidar": "^4.0.3",
25
33
  "fullscreen-ink": "^0.1.0",
26
34
  "ink": "^5.0.1",
27
35
  "meow": "^13.2.0",