Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-ravens-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"t3code-cli": patch
---

add conversation content search command
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ t3cli start [message]

```sh
t3cli list [--project <ref>] [--archived | --all]
t3cli search <query> [--limit <1-50>] # Search conversation content
t3cli show [--thread <id>] # Show thread details
t3cli send [--thread <id>] [message] # Send message to thread
t3cli transcript [--thread <id>] [--limit] # View messages
Expand Down
1 change: 1 addition & 0 deletions skills/t3code-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ Use cases: handoff long tasks, parallel work notifications, async workflows.

```sh
t3cli list --format json
t3cli search "conversation text" --format json
t3cli transcript --thread <id> --format json
printf '%s' "$PROMPT" | t3cli start --stdin --format json
```
Expand Down
3 changes: 2 additions & 1 deletion skills/t3code-cli/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ t3cli
├── env list|use|remove
├── project list|add|delete
├── model list
├── list|start|send|show|transcript|wait
├── list|search|start|send|show|transcript|wait
├── terminal list|create|attach|read|stream|wait|write|destroy
└── thread approve|respond|archive|interrupt|unarchive|update|delete|callback
```
Expand Down Expand Up @@ -96,6 +96,7 @@ Mutations dispatch `project.meta.update` with the next full scripts array and wa

```sh
t3cli list [--project <ref>] [--archived | --all] [--format json]
t3cli search <query> [--limit <1-50>] [--format auto|human|json]

t3cli start [message]
[--project <ref>] [--stdin] [--title <title>] [--worktree <path>]
Expand Down
1 change: 1 addition & 0 deletions src/application/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ export type {
WaitEvent,
} from "./service.ts";
export type { ApplicationError } from "./error.ts";
export type { ThreadSearchResult } from "./threads.ts";
6 changes: 5 additions & 1 deletion src/application/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
ModelSelection,
OrchestrationMessage,
OrchestrationProjectShell,
OrchestrationSearchThreadsInput,
OrchestrationShellSnapshot,
OrchestrationThread,
OrchestrationThreadShell,
Expand All @@ -20,7 +21,7 @@ import type {
} from "@t3tools/contracts";

import type { ApplicationError } from "./error.ts";
import type { ThreadShow } from "./threads.ts";
import type { ThreadSearchResult, ThreadShow } from "./threads.ts";

export type StartThreadInput = {
readonly projectRef?: string;
Expand Down Expand Up @@ -196,6 +197,9 @@ export class T3ProjectApplication extends Context.Service<
>()("t3cli/T3ProjectApplication") {}

export type T3ThreadApplicationService = {
readonly searchThreads: (
input: OrchestrationSearchThreadsInput,
) => Effect.Effect<ReadonlyArray<ThreadSearchResult>, ApplicationError>;
readonly listThreads: (
projectRef: string,
options?: {
Expand Down
44 changes: 43 additions & 1 deletion src/application/threads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import { resolveProjectScope } from "../domain/helpers.ts";
import { type ListThreadsInclude, type StartThreadInput } from "./service.ts";
import type { CallbackThreadInput, SendThreadInput } from "./service.ts";
import type { T3ThreadApplicationService } from "./service.ts";
import type { OrchestrationThreadShell } from "@t3tools/contracts";
import type {
OrchestrationSearchThreadsInput,
OrchestrationThreadSearchMatch,
OrchestrationThreadShell,
} from "@t3tools/contracts";
import { mergeModelOptions } from "./model-selection.ts";
import { derivePendingApprovals, derivePendingUserInputs } from "../domain/thread-activities.ts";
import {
Expand Down Expand Up @@ -73,6 +77,30 @@ export const makeThreadApplication = Effect.fn("makeThreadApplication")(function
threads: snapshot.threads.filter((thread) => thread.projectId === scope.project.id),
};
});
const searchThreads = Effect.fn("T3ApplicationLive.searchThreads")(function* (
input: OrchestrationSearchThreadsInput,
) {
const result = yield* orchestration.searchThreads(input);
const snapshot = yield* orchestration.getShellSnapshot();
const threadsById = new Map(snapshot.threads.map((thread) => [thread.id, thread]));
Comment on lines +84 to +85

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include archived threads when enriching search results

When a search match belongs to an archived thread, getShellSnapshot() cannot supply its shell metadata because this repository explicitly loads archived threads through getArchivedShellSnapshot() (see loadThreadsSnapshot). Consequently, valid matches are returned with threadTitle, branch, and worktreePath set to null, and human output displays - for those fields. Enrich search matches from a merged active-and-archived snapshot instead.

Useful? React with 👍 / 👎.

const projectsById = new Map(snapshot.projects.map((project) => [project.id, project]));
return result.matches.map((match) => {
const thread = threadsById.get(match.threadId);
const project = projectsById.get(match.projectId);
return {
threadId: match.threadId,
threadTitle: thread?.title ?? null,
projectId: match.projectId,
projectTitle: project?.title ?? null,
workspaceRoot: project?.workspaceRoot ?? null,
branch: thread?.branch ?? null,
worktreePath: thread?.worktreePath ?? null,
source: match.source,
snippet: match.snippet,
messageCreatedAt: match.messageCreatedAt,
} satisfies ThreadSearchResult;
});
});
const getThreadMessages = Effect.fn("T3ApplicationLive.getThreadMessages")(function* (
threadId: string,
) {
Expand Down Expand Up @@ -276,6 +304,7 @@ export const makeThreadApplication = Effect.fn("makeThreadApplication")(function
updateThread,
unarchiveThread,
listThreads,
searchThreads,
getThreadMessages,
respondToThread,
sendThread,
Expand All @@ -287,6 +316,19 @@ export const makeThreadApplication = Effect.fn("makeThreadApplication")(function
} satisfies T3ThreadApplicationService;
});

export type ThreadSearchResult = {
readonly threadId: OrchestrationThreadSearchMatch["threadId"];
readonly threadTitle: string | null;
readonly projectId: OrchestrationThreadSearchMatch["projectId"];
readonly projectTitle: string | null;
readonly workspaceRoot: string | null;
readonly branch: string | null;
readonly worktreePath: string | null;
readonly source: OrchestrationThreadSearchMatch["source"];
readonly snippet: OrchestrationThreadSearchMatch["snippet"];
readonly messageCreatedAt: OrchestrationThreadSearchMatch["messageCreatedAt"];
};

const loadThreadsSnapshot = Effect.fn("loadThreadsSnapshot")(function* (
include: ListThreadsInclude,
) {
Expand Down
2 changes: 2 additions & 0 deletions src/cli/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { cliEnvironmentSetting } from "./env/flag.ts";
import { createTerminalCommandGroup } from "./terminal.ts";
import { createModelCommand } from "./model.ts";
import { createProjectCommand } from "./project.ts";
import { searchThreadsCommand } from "./search.ts";
import { createThreadCommand } from "./thread.ts";
import { listThreadsCommand } from "./threads/list.ts";
import { sendThreadCommand } from "./threads/send.ts";
Expand All @@ -26,6 +27,7 @@ export function createCliCommand() {
listThreadsCommand,
createModelCommand(),
createProjectCommand(),
searchThreadsCommand,
createTerminalCommandGroup(),
startThreadCommand,
sendThreadCommand,
Expand Down
24 changes: 23 additions & 1 deletion src/cli/format/thread.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ThreadShow } from "../../application/threads.ts";
import type { ThreadSearchResult, ThreadShow } from "../../application/threads.ts";
import type { WaitEvent } from "../../application/service.ts";
import type { OrchestrationThread, OrchestrationThreadShell } from "@t3tools/contracts";
import { latestAssistantMessage, threadStatus } from "../../domain/thread-lifecycle.ts";
Expand Down Expand Up @@ -98,6 +98,28 @@ export function formatThreadsHuman(threads: ReadonlyArray<OrchestrationThreadShe
)}\n`;
}

export function formatThreadSearchHuman(matches: ReadonlyArray<ThreadSearchResult>) {
if (matches.length === 0) {
return "no matches\n";
}
return `${matches
.map((match) =>
formatRecord([
{ field: "thread", value: match.threadTitle ?? "-" },
{ field: "thread id", value: match.threadId },
{ field: "project", value: match.projectTitle ?? "-" },
{ field: "project id", value: match.projectId },
{ field: "workspace", value: match.workspaceRoot ?? "-" },
{ field: "branch", value: match.branch ?? "-" },
{ field: "worktree", value: match.worktreePath ?? "-" },
{ field: "source", value: match.source },
{ field: "created", value: match.messageCreatedAt ?? "-" },
{ field: "snippet", value: match.snippet },
]),
)
.join("\n\n")}\n`;
}

export function formatThreadDeletedHuman(input: {
readonly threadId: string;
readonly dispatch: { readonly sequence: number };
Expand Down
48 changes: 48 additions & 0 deletions src/cli/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as Effect from "effect/Effect";
import * as Option from "effect/Option";
import { Argument, Command, Flag } from "effect/unstable/cli";

import { T3Application } from "../application/service.ts";
import { loadT3CliEnv } from "../config/env/env.ts";
import { InvalidLimitError } from "./error.ts";
import { extraArgsConfig } from "./extra-args.ts";
import { formatFlag } from "./flags.ts";
import { formatThreadSearchHuman } from "./format/thread.ts";
import { resolveOutputFormat } from "./format/output.ts";
import { T3Output } from "./output/service.ts";
import { CliRuntime } from "./runtime/service.ts";

export const searchThreadsCommand = Command.make(
"search",
{
query: Argument.string("query"),
limit: Flag.integer("limit").pipe(
Flag.withDescription("Maximum matches (1-50, default: 50)"),
Flag.optional,
),
format: formatFlag,
...extraArgsConfig,
},
({ query, limit, format }) =>
Effect.gen(function* () {
const value = Option.getOrUndefined(limit);
if (value !== undefined && (value < 1 || value > 50)) {
return yield* Effect.fail(
new InvalidLimitError({ message: `invalid limit: ${value}`, value: String(value) }),
);
}
const application = yield* T3Application;
const cliRuntime = yield* CliRuntime;
const t3CliEnv = yield* loadT3CliEnv;
const output = yield* T3Output;
const resolvedFormat = resolveOutputFormat(format, cliRuntime, t3CliEnv, "json");
const matches = yield* application.searchThreads({
query,
...(value !== undefined ? { limit: value } : {}),
});
if (resolvedFormat === "json") {
return yield* output.printJson(matches);
}
return yield* output.writeStdout(formatThreadSearchHuman(matches));
}),
).pipe(Command.withDescription("search threads by conversation content"));
6 changes: 6 additions & 0 deletions src/orchestration/layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ export const makeT3Orchestration = Effect.fn("makeT3Orchestration")(function* ()
);
},
);
const searchThreads = Effect.fn("T3OrchestrationLive.searchThreads")(function* (input) {
return yield* rpc.run(ORCHESTRATION_WS_METHODS.searchThreads, (client) =>
client[ORCHESTRATION_WS_METHODS.searchThreads](input),
);
});
const getThreadSnapshot = Effect.fn("T3OrchestrationLive.getThreadSnapshot")(function* (
threadId: string,
) {
Expand Down Expand Up @@ -125,6 +130,7 @@ export const makeT3Orchestration = Effect.fn("makeT3Orchestration")(function* ()
getServerConfig,
getShellSnapshot,
getArchivedShellSnapshot,
searchThreads,
getThreadSnapshot,
watchShellSequence,
watchThreadItems,
Expand Down
5 changes: 5 additions & 0 deletions src/orchestration/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import type {
DispatchResult,
OrchestrationEvent,
OrchestrationShellSnapshot,
OrchestrationSearchThreadsInput,
OrchestrationSearchThreadsResult,
OrchestrationThread,
OrchestrationThreadStreamItem,
ServerProviders,
Expand Down Expand Up @@ -35,6 +37,9 @@ export type Orchestration = {
OrchestrationShellSnapshot,
OrchestrationError
>;
readonly searchThreads: (
input: OrchestrationSearchThreadsInput,
) => Effect.Effect<OrchestrationSearchThreadsResult, OrchestrationError>;
readonly getThreadSnapshot: (
threadId: string,
) => Effect.Effect<OrchestrationThread, OrchestrationError>;
Expand Down
2 changes: 2 additions & 0 deletions src/rpc/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
KeybindingsConfigError,
OrchestrationDispatchCommandError,
OrchestrationGetSnapshotError,
OrchestrationSearchThreadsError,
ServerSettingsError,
TerminalError,
} from "@t3tools/contracts";
Expand All @@ -21,6 +22,7 @@ const RpcErrorCauseSchema = Schema.Union([
KeybindingsConfigError,
OrchestrationDispatchCommandError,
OrchestrationGetSnapshotError,
OrchestrationSearchThreadsError,
ServerSettingsError,
TerminalError,
ConnectionBlockedError,
Expand Down
2 changes: 2 additions & 0 deletions src/rpc/operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
KeybindingsConfigError,
OrchestrationDispatchCommandError,
OrchestrationGetSnapshotError,
OrchestrationSearchThreadsError,
ServerSettingsError,
TerminalError,
} from "@t3tools/contracts";
Expand All @@ -22,6 +23,7 @@ export type CliRpcOperationError =
| KeybindingsConfigError
| OrchestrationDispatchCommandError
| OrchestrationGetSnapshotError
| OrchestrationSearchThreadsError
| RpcClientError.RpcClientError
| ServerSettingsError
| TerminalError;
Expand Down
2 changes: 2 additions & 0 deletions src/rpc/ws-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
WS_METHODS,
WsOrchestrationDispatchCommandRpc,
WsOrchestrationGetArchivedShellSnapshotRpc,
WsOrchestrationSearchThreadsRpc,
WsOrchestrationSubscribeShellRpc,
WsOrchestrationSubscribeThreadRpc,
WsServerProbeRpc,
Expand Down Expand Up @@ -46,6 +47,7 @@ export const CliWsRpcGroup = RpcGroup.make(
WsSubscribeTerminalMetadataRpc,
WsOrchestrationDispatchCommandRpc,
WsOrchestrationGetArchivedShellSnapshotRpc,
WsOrchestrationSearchThreadsRpc,
WsOrchestrationSubscribeShellRpc,
WsOrchestrationSubscribeThreadRpc,
WsServerProbeRpc,
Expand Down
2 changes: 1 addition & 1 deletion upstream-t3code
Submodule upstream-t3code updated 138 files
Loading