opencode-sesh 0.1.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.
- package/AGENTS.md +129 -0
- package/CHANGELOG.md +55 -0
- package/LICENSE +21 -0
- package/README.md +207 -0
- package/bin/sesh +26 -0
- package/bin/sesh-delete.sh +66 -0
- package/bin/sesh-list.sh +294 -0
- package/bin/sesh-preview.sh +68 -0
- package/bin/sesh-refresh-worker.sh +162 -0
- package/bin/sesh-shortcuts.sh +17 -0
- package/bin/sesh.sh +150 -0
- package/install.sh +241 -0
- package/opencode/tools/sesh-list.ts +42 -0
- package/package.json +57 -0
- package/themes/glow-dark-clean.json +191 -0
- package/tui/sesh-panel.tsx +1177 -0
package/bin/sesh.sh
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Pick an opencode session. Native to opencode, terminal-agnostic: no iTerm2,
|
|
3
|
+
# no AppleScript, no panes, no window management. Stock fzf is the only UI.
|
|
4
|
+
# Refreshes publish one state-dir snapshot; fzf query changes render that
|
|
5
|
+
# snapshot only, so typing never starts competing scans.
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
|
|
10
|
+
resolve_tool() {
|
|
11
|
+
local configured=$1 fallback=$2
|
|
12
|
+
if [ -n "$configured" ]; then
|
|
13
|
+
case "$configured" in /*) [ -x "$configured" ] && { printf '%s\n' "$configured"; return; };; esac
|
|
14
|
+
return 1
|
|
15
|
+
fi
|
|
16
|
+
command -v "$fallback"
|
|
17
|
+
}
|
|
18
|
+
SESH_FZF=$(resolve_tool "${SESH_FZF:-}" fzf) || { echo 'sesh: fzf executable unavailable' >&2; exit 1; }
|
|
19
|
+
SESH_JQ=$(resolve_tool "${SESH_JQ:-}" jq) || { echo 'sesh: jq executable unavailable' >&2; exit 1; }
|
|
20
|
+
export SESH_FZF SESH_JQ
|
|
21
|
+
OPENCODE_BIN=${SESH_OPENCODE:-opencode}
|
|
22
|
+
|
|
23
|
+
version=$("$SESH_FZF" --version 2>/dev/null || true)
|
|
24
|
+
# --bind transform: and start,every():reload-sync need a modern stock fzf.
|
|
25
|
+
if [[ "$version" =~ ^([0-9]+)\.([0-9]+)(\.([0-9]+))? ]]; then
|
|
26
|
+
fzf_major=$((10#${BASH_REMATCH[1]}))
|
|
27
|
+
fzf_minor=$((10#${BASH_REMATCH[2]}))
|
|
28
|
+
if [ "$fzf_major" -eq 0 ] && [ "$fzf_minor" -lt 52 ]; then
|
|
29
|
+
echo "sesh: fzf >= 0.52.0 is required (found $version)" >&2
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
else
|
|
33
|
+
echo "sesh: cannot parse fzf version from: $version" >&2
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
if [ "${1:-}" = --check ]; then
|
|
38
|
+
"$SESH_JQ" -en '"2026-01-01T00:00:00Z" | fromdateiso8601 | type == "number"' >/dev/null || { echo 'jq capability check failed' >&2; exit 1; }
|
|
39
|
+
command -v sqlite3 >/dev/null 2>&1 || command -v "$OPENCODE_BIN" >/dev/null 2>&1 || { echo 'neither sqlite3 nor opencode is available' >&2; exit 1; }
|
|
40
|
+
echo 'sesh: dependencies ready'
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
scope_value=''
|
|
45
|
+
picker_limit=0
|
|
46
|
+
picker_archived=0
|
|
47
|
+
picker_print=0
|
|
48
|
+
picker_fork=0
|
|
49
|
+
picker_query=''
|
|
50
|
+
while [ "$#" -gt 0 ]; do
|
|
51
|
+
case "$1" in
|
|
52
|
+
--cwd) scope_value=$PWD ;;
|
|
53
|
+
--limit) picker_limit=${2:-}; shift ;;
|
|
54
|
+
--archived) picker_archived=1 ;;
|
|
55
|
+
--print) picker_print=1 ;;
|
|
56
|
+
--fork) picker_fork=1 ;;
|
|
57
|
+
--query) picker_query=${2:-}; shift ;;
|
|
58
|
+
*) echo "unknown option: $1" >&2; exit 2 ;;
|
|
59
|
+
esac
|
|
60
|
+
shift
|
|
61
|
+
done
|
|
62
|
+
case "$picker_limit" in ''|*[!0-9]*) echo "--limit must be a non-negative integer" >&2; exit 2;; esac
|
|
63
|
+
|
|
64
|
+
shell_quote() {
|
|
65
|
+
# Quotes fixed command arguments embedded in fzf's shell action strings.
|
|
66
|
+
# Selection data never crosses this boundary; fzf supplies {q}/{2} safely.
|
|
67
|
+
printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\"'\\\"'/g")"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
state_dir=$(mktemp -d "${TMPDIR:-/tmp}/sesh.XXXXXX")
|
|
71
|
+
chmod 700 "$state_dir"
|
|
72
|
+
|
|
73
|
+
q_list=$(shell_quote "$SCRIPT_DIR/sesh-list.sh")
|
|
74
|
+
q_worker=$(shell_quote "$SCRIPT_DIR/sesh-refresh-worker.sh")
|
|
75
|
+
q_preview=$(shell_quote "$SCRIPT_DIR/sesh-preview.sh")
|
|
76
|
+
q_delete=$(shell_quote "$SCRIPT_DIR/sesh-delete.sh")
|
|
77
|
+
q_shortcuts=$(shell_quote "$SCRIPT_DIR/sesh-shortcuts.sh")
|
|
78
|
+
q_state=$(shell_quote "$state_dir")
|
|
79
|
+
export SESH_LIST="$SCRIPT_DIR/sesh-list.sh"
|
|
80
|
+
# The shared worker persists scope/limit; the scope toggle and the archived
|
|
81
|
+
# filter need this picker's directory and flags too, so merge them up front.
|
|
82
|
+
"$SCRIPT_DIR/sesh-refresh-worker.sh" init --state-dir "$state_dir" --scope "$scope_value" --limit "$picker_limit"
|
|
83
|
+
"$SESH_JQ" -c --arg cwd "$PWD" --argjson archived "$picker_archived" '.cwd = $cwd | .archived = $archived' "$state_dir/settings.json" > "$state_dir/settings.tmp" && mv "$state_dir/settings.tmp" "$state_dir/settings.json"
|
|
84
|
+
"$SCRIPT_DIR/sesh-refresh-worker.sh" worker --state-dir "$state_dir" &
|
|
85
|
+
worker_pid=$!
|
|
86
|
+
cleanup() {
|
|
87
|
+
# Reap this picker's exact worker before its private state disappears.
|
|
88
|
+
kill -TERM "$worker_pid" 2>/dev/null || true
|
|
89
|
+
wait "$worker_pid" 2>/dev/null || true
|
|
90
|
+
rm -rf "$state_dir"
|
|
91
|
+
}
|
|
92
|
+
trap cleanup EXIT
|
|
93
|
+
trap 'exit 130' HUP INT TERM
|
|
94
|
+
|
|
95
|
+
cache_cmd="$q_worker render --state-dir $q_state --query {q}"
|
|
96
|
+
preview_cmd="if [ -f $q_state/help ]; then $q_shortcuts; else $q_preview $q_state {2}; fi"
|
|
97
|
+
help_bind="?:transform:[ -z {q} ] && ( [ -f $q_state/help ] && rm -f $q_state/help && echo hide-preview || ( : > $q_state/help && echo show-preview+refresh-preview ) ) || echo 'put(?)'"
|
|
98
|
+
toggle_cmd="$q_list --toggle-scope --state-dir $q_state >/dev/null"
|
|
99
|
+
delete_cmd="$q_delete $q_state {2}"
|
|
100
|
+
|
|
101
|
+
query="$picker_query"
|
|
102
|
+
while :; do
|
|
103
|
+
selection=''
|
|
104
|
+
set +e
|
|
105
|
+
selection=$(FZF_DEFAULT_COMMAND='printf ""' FZF_DEFAULT_OPTS='' FZF_DEFAULT_OPTS_FILE='' "$SESH_FZF" --print-query --expect=ctrl-f --query="$query" --ansi --delimiter=$'\t' --with-nth=4 --disabled --layout=reverse --info=hidden --prompt='❯ ' \
|
|
106
|
+
--preview="$preview_cmd" --preview-window='right,50%,wrap,hidden' \
|
|
107
|
+
--bind='space:transform:[ -z {q} ] && echo toggle-preview || echo "put( )"' \
|
|
108
|
+
--bind="$help_bind" \
|
|
109
|
+
--bind="ctrl-x:execute($delete_cmd)+reload($cache_cmd)" \
|
|
110
|
+
--bind="ctrl-g:execute($toggle_cmd)+reload($cache_cmd)" \
|
|
111
|
+
--bind="change:reload:$cache_cmd" \
|
|
112
|
+
--bind="start,every(3):reload-sync:$cache_cmd")
|
|
113
|
+
status=$?
|
|
114
|
+
set -e
|
|
115
|
+
case "$status" in 0) ;; 1|130) exit 0;; *) echo "sesh: picker failed ($status)" >&2; exit "$status";; esac
|
|
116
|
+
key=${selection%%$'\n'*}
|
|
117
|
+
selection=${selection#*$'\n'}
|
|
118
|
+
query=${selection%%$'\n'*}
|
|
119
|
+
selection=${selection#*$'\n'}
|
|
120
|
+
session_id=$(printf '%s\n' "$selection" | cut -f2)
|
|
121
|
+
[ -n "$session_id" ] || continue
|
|
122
|
+
# Dispatch queues a new worker scan and waits for that exact serialized poll.
|
|
123
|
+
# It cannot accept a previously fresh status while the worker owns its lock.
|
|
124
|
+
poll_target=$("$SCRIPT_DIR/sesh-refresh-worker.sh" request --state-dir "$state_dir")
|
|
125
|
+
if ! "$SCRIPT_DIR/sesh-refresh-worker.sh" wait --state-dir "$state_dir" --target "$poll_target" --timeout 12; then
|
|
126
|
+
echo 'Live state unavailable; retry selection after refresh' >&2
|
|
127
|
+
continue
|
|
128
|
+
fi
|
|
129
|
+
record=$("$SESH_JQ" -c --arg sid "$session_id" 'select(.sessionId==$sid)' "$state_dir/snapshot.jsonl")
|
|
130
|
+
[ -n "$record" ] || continue
|
|
131
|
+
cwd=$(printf '%s' "$record" | "$SESH_JQ" -r '.cwd')
|
|
132
|
+
fork=0
|
|
133
|
+
if [ "$picker_fork" = 1 ] || [ "$key" = ctrl-f ]; then fork=1; fi
|
|
134
|
+
if [ "$picker_print" = 1 ]; then
|
|
135
|
+
if [ "$fork" = 1 ]; then printf '%s\t%s\tfork\n' "$session_id" "$cwd"; else printf '%s\t%s\n' "$session_id" "$cwd"; fi
|
|
136
|
+
exit 0
|
|
137
|
+
fi
|
|
138
|
+
if [ ! -d "$cwd" ]; then
|
|
139
|
+
echo 'sesh: session directory is unavailable; returning to the picker.' >&2
|
|
140
|
+
continue
|
|
141
|
+
fi
|
|
142
|
+
command -v "$OPENCODE_BIN" >/dev/null 2>&1 || { echo 'sesh: opencode executable unavailable' >&2; exit 1; }
|
|
143
|
+
# Resume in place: this terminal becomes the session. No tabs, no panes.
|
|
144
|
+
if [ "$fork" = 1 ]; then
|
|
145
|
+
( cd "$cwd" && exec "$OPENCODE_BIN" --session "$session_id" --fork )
|
|
146
|
+
else
|
|
147
|
+
( cd "$cwd" && exec "$OPENCODE_BIN" --session "$session_id" )
|
|
148
|
+
fi
|
|
149
|
+
echo 'sesh: opencode exited; returning to the picker.' >&2
|
|
150
|
+
done
|
package/install.sh
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Install sesh for the current user.
|
|
3
|
+
# Terminal- and OS-agnostic: macOS and Linux, any terminal. No administrator
|
|
4
|
+
# privileges, downloads, or shell-rc edits. Runtime requires jq, fzf >= 0.52
|
|
5
|
+
# and sqlite3 or the opencode CLI; the in-TUI panel additionally needs the
|
|
6
|
+
# packages opencode installs for local plugins (handled below).
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
fail() { printf 'sesh install: %s\n' "$*" >&2; exit 1; }
|
|
10
|
+
note() { printf 'sesh install: %s\n' "$*"; }
|
|
11
|
+
|
|
12
|
+
SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
13
|
+
|
|
14
|
+
# Follow the same XDG locations opencode itself reads.
|
|
15
|
+
CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
|
|
16
|
+
CONFIG_DIR="$CONFIG_HOME/opencode"
|
|
17
|
+
BIN_DIR="${XDG_BIN_HOME:-$HOME/.local/bin}"
|
|
18
|
+
|
|
19
|
+
INSTALL_TOOL=1
|
|
20
|
+
INSTALL_TUI=1
|
|
21
|
+
UNINSTALL=0
|
|
22
|
+
|
|
23
|
+
# Fallback plugin version for when the opencode CLI is unavailable; the
|
|
24
|
+
# installer prefers the local opencode version so the panel matches its SDK.
|
|
25
|
+
PLUGIN_VERSION_FALLBACK="1.18.30"
|
|
26
|
+
# Peer line the panel was developed and typechecked against. A checkout's
|
|
27
|
+
# package.json overrides these, so dependency bumps cannot drift the installed
|
|
28
|
+
# config out of sync (solid-js moves with OpenTUI via @opentui/keymap's peer).
|
|
29
|
+
OPENTUI_RANGE="^0.5.11"
|
|
30
|
+
SOLID_VERSION="1.9.12"
|
|
31
|
+
if [ -f "$SOURCE_DIR/package.json" ] && command -v jq >/dev/null 2>&1; then
|
|
32
|
+
opentui_range=$(jq -r '.devDependencies["@opentui/solid"] // empty' "$SOURCE_DIR/package.json" 2>/dev/null || true)
|
|
33
|
+
solid_version=$(jq -r '.devDependencies["solid-js"] // empty' "$SOURCE_DIR/package.json" 2>/dev/null || true)
|
|
34
|
+
if [ -n "$opentui_range" ]; then OPENTUI_RANGE=$opentui_range; fi
|
|
35
|
+
if [ -n "$solid_version" ]; then SOLID_VERSION=$solid_version; fi
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
usage() {
|
|
39
|
+
cat <<'USAGE'
|
|
40
|
+
Usage: bash install.sh [--bin-dir DIR] [--no-tool] [--no-tui] [--uninstall]
|
|
41
|
+
|
|
42
|
+
--bin-dir DIR Directory for the sesh launcher (default: ~/.local/bin).
|
|
43
|
+
--no-tool Skip installing the sesh-list custom tool.
|
|
44
|
+
--no-tui Skip installing the in-TUI sessions panel plugin.
|
|
45
|
+
--uninstall Remove everything this script installed.
|
|
46
|
+
--help Show this help.
|
|
47
|
+
USAGE
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
while [ "$#" -gt 0 ]; do
|
|
51
|
+
case "$1" in
|
|
52
|
+
--bin-dir) [ "$#" -ge 2 ] || fail "--bin-dir needs a directory."; BIN_DIR="$2"; shift 2 ;;
|
|
53
|
+
--no-tool) INSTALL_TOOL=0; shift ;;
|
|
54
|
+
--no-tui) INSTALL_TUI=0; shift ;;
|
|
55
|
+
--uninstall) UNINSTALL=1; shift ;;
|
|
56
|
+
--help) usage; exit 0 ;;
|
|
57
|
+
*) fail "unknown option: $1" ;;
|
|
58
|
+
esac
|
|
59
|
+
done
|
|
60
|
+
|
|
61
|
+
LAUNCHER="$BIN_DIR/sesh"
|
|
62
|
+
# `/sesh` is registered by the TUI plugin (a markdown command cannot open the
|
|
63
|
+
# dialog); this path only exists so we can clean up an obsolete install.
|
|
64
|
+
COMMAND_DST="$CONFIG_DIR/commands/sesh.md"
|
|
65
|
+
TOOL_DST="$CONFIG_DIR/tools/sesh-list.ts"
|
|
66
|
+
PANEL_DST="$CONFIG_DIR/plugins/sesh-panel.tsx"
|
|
67
|
+
PACKAGE_JSON="$CONFIG_DIR/package.json"
|
|
68
|
+
TUI_JSON="$CONFIG_DIR/tui.json"
|
|
69
|
+
|
|
70
|
+
# Remove a file only when it carries our marker, so a user's own file with a
|
|
71
|
+
# colliding name is never destroyed.
|
|
72
|
+
remove_if_marker() {
|
|
73
|
+
local file=$1 marker=$2
|
|
74
|
+
if [ -f "$file" ] && grep -qF "$marker" "$file" 2>/dev/null; then
|
|
75
|
+
rm -f "$file"
|
|
76
|
+
return 0
|
|
77
|
+
fi
|
|
78
|
+
return 1
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
remove_tui_entry() {
|
|
82
|
+
[ -f "$TUI_JSON" ] || return 0
|
|
83
|
+
command -v jq >/dev/null 2>&1 || return 0
|
|
84
|
+
local tmp
|
|
85
|
+
tmp=$(mktemp "${TMPDIR:-/tmp}/sesh-tui.XXXXXX") || return 0
|
|
86
|
+
if jq 'if (.plugin | type) == "array" then .plugin = [.plugin[] | select(. != "./plugins/sesh-panel.tsx")] else . end' "$TUI_JSON" > "$tmp" 2>/dev/null; then
|
|
87
|
+
cmp -s "$TUI_JSON" "$tmp" || { cp -p "$TUI_JSON" "$TUI_JSON.bak.$(date +%Y%m%d%H%M%S)"; mv "$tmp" "$TUI_JSON"; }
|
|
88
|
+
rm -f "$tmp"
|
|
89
|
+
else
|
|
90
|
+
rm -f "$tmp"
|
|
91
|
+
fi
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if [ "$UNINSTALL" = 1 ]; then
|
|
95
|
+
[ -L "$LAUNCHER" ] && rm -f "$LAUNCHER" && note "removed $LAUNCHER"
|
|
96
|
+
remove_if_marker "$COMMAND_DST" "rich interactive picker" && note "removed $COMMAND_DST"
|
|
97
|
+
remove_if_marker "$TOOL_DST" "list opencode sessions for the agent" && note "removed $TOOL_DST"
|
|
98
|
+
remove_if_marker "$PANEL_DST" "@jsxImportSource @opentui/solid" && note "removed $PANEL_DST"
|
|
99
|
+
remove_tui_entry && note "unregistered the panel from $TUI_JSON"
|
|
100
|
+
note "uninstalled (extraction cache and plugin dependencies left intact)"
|
|
101
|
+
exit 0
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
for path in \
|
|
105
|
+
bin/sesh \
|
|
106
|
+
bin/sesh.sh \
|
|
107
|
+
bin/sesh-list.sh \
|
|
108
|
+
bin/sesh-preview.sh \
|
|
109
|
+
bin/sesh-delete.sh \
|
|
110
|
+
bin/sesh-refresh-worker.sh \
|
|
111
|
+
bin/sesh-shortcuts.sh \
|
|
112
|
+
opencode/tools/sesh-list.ts \
|
|
113
|
+
tui/sesh-panel.tsx \
|
|
114
|
+
themes/glow-dark-clean.json; do
|
|
115
|
+
[ -f "$SOURCE_DIR/$path" ] || fail "installer bundle is incomplete: missing $path."
|
|
116
|
+
done
|
|
117
|
+
|
|
118
|
+
for tool in bash jq fzf; do
|
|
119
|
+
command -v "$tool" >/dev/null 2>&1 || fail "required tool '$tool' is unavailable."
|
|
120
|
+
done
|
|
121
|
+
fzf_version=$(fzf --version 2>/dev/null || true)
|
|
122
|
+
if [[ "$fzf_version" =~ ^([0-9]+)\.([0-9]+)(\.([0-9]+))? ]]; then
|
|
123
|
+
if [ "$((10#${BASH_REMATCH[1]}))" -eq 0 ] && [ "$((10#${BASH_REMATCH[2]}))" -lt 52 ]; then
|
|
124
|
+
fail "fzf >= 0.52.0 is required (found $fzf_version)."
|
|
125
|
+
fi
|
|
126
|
+
else
|
|
127
|
+
fail "cannot parse fzf version from: $fzf_version."
|
|
128
|
+
fi
|
|
129
|
+
command -v sqlite3 >/dev/null 2>&1 || command -v opencode >/dev/null 2>&1 \
|
|
130
|
+
|| fail "either sqlite3 or the opencode CLI is required."
|
|
131
|
+
command -v opencode >/dev/null 2>&1 || note "opencode CLI not found: listing and previews work via sqlite3, but resume and delete need opencode."
|
|
132
|
+
|
|
133
|
+
umask 077
|
|
134
|
+
mkdir -p "$BIN_DIR"
|
|
135
|
+
if [ -e "$LAUNCHER" ] && [ ! -L "$LAUNCHER" ]; then
|
|
136
|
+
fail "$LAUNCHER already exists and is not a symlink. Move it aside, then retry."
|
|
137
|
+
fi
|
|
138
|
+
ln -sfn "$SOURCE_DIR/bin/sesh" "$LAUNCHER"
|
|
139
|
+
note "linked $LAUNCHER -> $SOURCE_DIR/bin/sesh"
|
|
140
|
+
|
|
141
|
+
install_file() {
|
|
142
|
+
local src=$1 dst=$2
|
|
143
|
+
mkdir -p "$(dirname "$dst")"
|
|
144
|
+
if [ -f "$dst" ] && cmp -s "$src" "$dst"; then
|
|
145
|
+
note "up to date $dst"
|
|
146
|
+
return 0
|
|
147
|
+
fi
|
|
148
|
+
if [ -f "$dst" ]; then
|
|
149
|
+
cp -p "$dst" "$dst.bak.$(date +%Y%m%d%H%M%S)"
|
|
150
|
+
note "backed up existing $(basename "$dst")"
|
|
151
|
+
fi
|
|
152
|
+
cp -p "$src" "$dst"
|
|
153
|
+
note "installed $dst"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# Earlier versions shipped a markdown `/sesh` command that could only prompt the
|
|
157
|
+
# agent; the TUI plugin now registers the slash itself. Drop the old file so the
|
|
158
|
+
# two do not collide.
|
|
159
|
+
remove_if_marker "$COMMAND_DST" "rich interactive picker" && note "removed obsolete markdown command $COMMAND_DST"
|
|
160
|
+
[ "$INSTALL_TOOL" = 1 ] && install_file "$SOURCE_DIR/opencode/tools/sesh-list.ts" "$TOOL_DST"
|
|
161
|
+
|
|
162
|
+
opencode_version=''
|
|
163
|
+
if command -v opencode >/dev/null 2>&1; then
|
|
164
|
+
raw_version=$(opencode --version 2>/dev/null || true)
|
|
165
|
+
if [[ "$raw_version" =~ ([0-9]+\.[0-9]+\.[0-9]+) ]]; then opencode_version="${BASH_REMATCH[1]}"; fi
|
|
166
|
+
fi
|
|
167
|
+
plugin_version="${opencode_version:-$PLUGIN_VERSION_FALLBACK}"
|
|
168
|
+
|
|
169
|
+
if [ "$INSTALL_TUI" = 1 ]; then
|
|
170
|
+
install_file "$SOURCE_DIR/tui/sesh-panel.tsx" "$PANEL_DST"
|
|
171
|
+
|
|
172
|
+
# opencode has no persistent-sidebar API: the panel is an xlarge modal
|
|
173
|
+
# dialog (see tui/sesh-panel.tsx). Register it idempotently and drop the
|
|
174
|
+
# previous filename if an older install left it behind.
|
|
175
|
+
tui_created=0
|
|
176
|
+
if [ ! -f "$TUI_JSON" ]; then
|
|
177
|
+
printf '{\n "$schema": "https://opencode.ai/tui.json",\n "plugin": []\n}\n' > "$TUI_JSON"
|
|
178
|
+
tui_created=1
|
|
179
|
+
note "created $TUI_JSON"
|
|
180
|
+
fi
|
|
181
|
+
tui_tmp=$(mktemp "${TMPDIR:-/tmp}/sesh-tui.XXXXXX")
|
|
182
|
+
if jq '
|
|
183
|
+
(.plugin // []) as $plugins
|
|
184
|
+
| ($plugins | map(select(. != "./plugins/sessions-panel.tsx"))) as $clean
|
|
185
|
+
| .plugin = (if ($clean | index("./plugins/sesh-panel.tsx")) then $clean else $clean + ["./plugins/sesh-panel.tsx"] end)
|
|
186
|
+
' "$TUI_JSON" > "$tui_tmp" 2>/dev/null; then
|
|
187
|
+
if cmp -s "$TUI_JSON" "$tui_tmp"; then
|
|
188
|
+
note "tui.json already registers the sesh panel"
|
|
189
|
+
rm -f "$tui_tmp"
|
|
190
|
+
else
|
|
191
|
+
[ "$tui_created" = 1 ] || cp -p "$TUI_JSON" "$TUI_JSON.bak.$(date +%Y%m%d%H%M%S)"
|
|
192
|
+
mv "$tui_tmp" "$TUI_JSON"
|
|
193
|
+
note "registered ./plugins/sesh-panel.tsx in tui.json"
|
|
194
|
+
fi
|
|
195
|
+
else
|
|
196
|
+
rm -f "$tui_tmp"
|
|
197
|
+
note "warning: could not update $TUI_JSON; add \"./plugins/sesh-panel.tsx\" to its plugin list by hand"
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
# Local plugins resolve their imports from the config directory, and opencode
|
|
201
|
+
# runs `bun install` there at startup. Keep the required packages declared
|
|
202
|
+
# without clobbering versions the user already pinned.
|
|
203
|
+
pkg_created=0
|
|
204
|
+
if [ ! -f "$PACKAGE_JSON" ]; then
|
|
205
|
+
printf '{\n "dependencies": {}\n}\n' > "$PACKAGE_JSON"
|
|
206
|
+
pkg_created=1
|
|
207
|
+
note "created $PACKAGE_JSON"
|
|
208
|
+
fi
|
|
209
|
+
pkg_tmp=$(mktemp "${TMPDIR:-/tmp}/sesh-pkg.XXXXXX")
|
|
210
|
+
if jq --arg p "$plugin_version" --arg o "$OPENTUI_RANGE" --arg s "$SOLID_VERSION" '
|
|
211
|
+
.dependencies = ((.dependencies // {}) as $d
|
|
212
|
+
| {
|
|
213
|
+
"@opencode-ai/plugin": ($d["@opencode-ai/plugin"] // $p),
|
|
214
|
+
"@opentui/core": ($d["@opentui/core"] // $o),
|
|
215
|
+
"@opentui/keymap": ($d["@opentui/keymap"] // $o),
|
|
216
|
+
"@opentui/solid": ($d["@opentui/solid"] // $o),
|
|
217
|
+
"solid-js": ($d["solid-js"] // $s)
|
|
218
|
+
} + $d)
|
|
219
|
+
' "$PACKAGE_JSON" > "$pkg_tmp" 2>/dev/null; then
|
|
220
|
+
if cmp -s "$PACKAGE_JSON" "$pkg_tmp"; then
|
|
221
|
+
note "plugin dependencies already present in $PACKAGE_JSON"
|
|
222
|
+
rm -f "$pkg_tmp"
|
|
223
|
+
else
|
|
224
|
+
[ "$pkg_created" = 1 ] || cp -p "$PACKAGE_JSON" "$PACKAGE_JSON.bak.$(date +%Y%m%d%H%M%S)"
|
|
225
|
+
mv "$pkg_tmp" "$PACKAGE_JSON"
|
|
226
|
+
note "declared plugin dependencies in $PACKAGE_JSON (opencode installs them on next start)"
|
|
227
|
+
fi
|
|
228
|
+
else
|
|
229
|
+
rm -f "$pkg_tmp"
|
|
230
|
+
note "warning: could not update $PACKAGE_JSON; the TUI panel may fail to load"
|
|
231
|
+
fi
|
|
232
|
+
fi
|
|
233
|
+
|
|
234
|
+
"$LAUNCHER" --check || fail "post-install check failed."
|
|
235
|
+
note "done"
|
|
236
|
+
note " picker: run 'sesh' in any terminal, or '/sesh' inside opencode"
|
|
237
|
+
note " sidebar: restart opencode to load the panel (ctrl+o opens the full picker)"
|
|
238
|
+
case ":$PATH:" in
|
|
239
|
+
*":$BIN_DIR:"*) ;;
|
|
240
|
+
*) note " note: $BIN_DIR is not on PATH; add it to use the launcher anywhere." ;;
|
|
241
|
+
esac
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Custom tool: list opencode sessions for the agent.
|
|
2
|
+
// Install: copy to ~/.config/opencode/tools/sesh-list.ts (global) or
|
|
3
|
+
// .opencode/tools/sesh-list.ts (project). Requires the @opencode-ai/plugin
|
|
4
|
+
// package next to it — see https://opencode.ai/docs/custom-tools/
|
|
5
|
+
//
|
|
6
|
+
// Lets the agent browse previous work across ALL project directories and
|
|
7
|
+
// offer to resume one, without depending on the external fzf picker.
|
|
8
|
+
import { tool } from "@opencode-ai/plugin"
|
|
9
|
+
|
|
10
|
+
export default tool({
|
|
11
|
+
description: "List opencode sessions across all project directories, newest first",
|
|
12
|
+
args: {
|
|
13
|
+
limit: tool.schema
|
|
14
|
+
.number()
|
|
15
|
+
.describe("Maximum sessions to return")
|
|
16
|
+
.default(50),
|
|
17
|
+
directory: tool.schema
|
|
18
|
+
.string()
|
|
19
|
+
.describe("Only sessions from this directory (default: all directories)")
|
|
20
|
+
.optional(),
|
|
21
|
+
},
|
|
22
|
+
async execute(args) {
|
|
23
|
+
const bun = (globalThis as any).Bun
|
|
24
|
+
if (!bun || typeof bun.$ !== "function") {
|
|
25
|
+
throw new Error("This tool requires the Bun runtime (globalThis.Bun.$)")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const out = await bun.$`opencode session list -n ${args.limit} --format json`.json()
|
|
29
|
+
const sessions = (Array.isArray(out) ? out : []).filter(
|
|
30
|
+
(s) => !args.directory || s.directory === args.directory,
|
|
31
|
+
)
|
|
32
|
+
return JSON.stringify(
|
|
33
|
+
sessions.map((s) => ({
|
|
34
|
+
id: s.id,
|
|
35
|
+
title: s.title,
|
|
36
|
+
directory: s.directory,
|
|
37
|
+
updated: s.updated,
|
|
38
|
+
created: s.created,
|
|
39
|
+
})),
|
|
40
|
+
)
|
|
41
|
+
},
|
|
42
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-sesh",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Browse, search, preview and resume opencode sessions from the terminal and the TUI.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Ozan Dogrultan",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/ozandogrultan/opencode-sesh.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/ozandogrultan/opencode-sesh#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/ozandogrultan/opencode-sesh/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"opencode",
|
|
18
|
+
"sessions",
|
|
19
|
+
"session-browser",
|
|
20
|
+
"fzf",
|
|
21
|
+
"tui",
|
|
22
|
+
"cli"
|
|
23
|
+
],
|
|
24
|
+
"bin": {
|
|
25
|
+
"sesh": "bin/sesh"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"bin/",
|
|
29
|
+
"opencode/",
|
|
30
|
+
"themes/",
|
|
31
|
+
"tui/",
|
|
32
|
+
"install.sh",
|
|
33
|
+
"AGENTS.md",
|
|
34
|
+
"CHANGELOG.md"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=20"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"test": "bash tests/sesh.sh",
|
|
44
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
45
|
+
"lint:sh": "bash -n bin/*.sh bin/sesh install.sh tests/sesh.sh"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@opencode-ai/plugin": "1.18.30",
|
|
49
|
+
"@opencode-ai/sdk": "1.18.30",
|
|
50
|
+
"@opentui/core": "^0.5.11",
|
|
51
|
+
"@opentui/keymap": "^0.5.11",
|
|
52
|
+
"@opentui/solid": "^0.5.11",
|
|
53
|
+
"@types/node": "26.5.1",
|
|
54
|
+
"solid-js": "1.9.12",
|
|
55
|
+
"typescript": "7.0.2"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
{
|
|
2
|
+
"document": {
|
|
3
|
+
"block_prefix": "\n",
|
|
4
|
+
"block_suffix": "\n",
|
|
5
|
+
"color": "252",
|
|
6
|
+
"margin": 2
|
|
7
|
+
},
|
|
8
|
+
"block_quote": {
|
|
9
|
+
"indent": 1,
|
|
10
|
+
"indent_token": "\u2502 "
|
|
11
|
+
},
|
|
12
|
+
"paragraph": {},
|
|
13
|
+
"list": {
|
|
14
|
+
"level_indent": 2
|
|
15
|
+
},
|
|
16
|
+
"heading": {
|
|
17
|
+
"block_suffix": "\n",
|
|
18
|
+
"color": "39",
|
|
19
|
+
"bold": true
|
|
20
|
+
},
|
|
21
|
+
"h1": {
|
|
22
|
+
"prefix": " ",
|
|
23
|
+
"suffix": " ",
|
|
24
|
+
"color": "228",
|
|
25
|
+
"background_color": "63",
|
|
26
|
+
"bold": true
|
|
27
|
+
},
|
|
28
|
+
"h2": {
|
|
29
|
+
"prefix": ""
|
|
30
|
+
},
|
|
31
|
+
"h3": {
|
|
32
|
+
"prefix": ""
|
|
33
|
+
},
|
|
34
|
+
"h4": {
|
|
35
|
+
"prefix": ""
|
|
36
|
+
},
|
|
37
|
+
"h5": {
|
|
38
|
+
"prefix": ""
|
|
39
|
+
},
|
|
40
|
+
"h6": {
|
|
41
|
+
"prefix": "",
|
|
42
|
+
"color": "35",
|
|
43
|
+
"bold": false
|
|
44
|
+
},
|
|
45
|
+
"text": {},
|
|
46
|
+
"strikethrough": {
|
|
47
|
+
"crossed_out": true
|
|
48
|
+
},
|
|
49
|
+
"emph": {
|
|
50
|
+
"italic": true
|
|
51
|
+
},
|
|
52
|
+
"strong": {
|
|
53
|
+
"bold": true
|
|
54
|
+
},
|
|
55
|
+
"hr": {
|
|
56
|
+
"color": "240",
|
|
57
|
+
"format": "\n--------\n"
|
|
58
|
+
},
|
|
59
|
+
"item": {
|
|
60
|
+
"block_prefix": "\u2022 "
|
|
61
|
+
},
|
|
62
|
+
"enumeration": {
|
|
63
|
+
"block_prefix": ". "
|
|
64
|
+
},
|
|
65
|
+
"task": {
|
|
66
|
+
"ticked": "[\u2713] ",
|
|
67
|
+
"unticked": "[ ] "
|
|
68
|
+
},
|
|
69
|
+
"link": {
|
|
70
|
+
"color": "30",
|
|
71
|
+
"underline": true
|
|
72
|
+
},
|
|
73
|
+
"link_text": {
|
|
74
|
+
"color": "35",
|
|
75
|
+
"bold": true
|
|
76
|
+
},
|
|
77
|
+
"image": {
|
|
78
|
+
"color": "212",
|
|
79
|
+
"underline": true
|
|
80
|
+
},
|
|
81
|
+
"image_text": {
|
|
82
|
+
"color": "243",
|
|
83
|
+
"format": "Image: {{.text}} \u2192"
|
|
84
|
+
},
|
|
85
|
+
"code": {
|
|
86
|
+
"prefix": "\u00a0",
|
|
87
|
+
"suffix": "\u00a0",
|
|
88
|
+
"color": "203",
|
|
89
|
+
"background_color": "236"
|
|
90
|
+
},
|
|
91
|
+
"code_block": {
|
|
92
|
+
"color": "244",
|
|
93
|
+
"margin": 2,
|
|
94
|
+
"chroma": {
|
|
95
|
+
"text": {
|
|
96
|
+
"color": "#C4C4C4"
|
|
97
|
+
},
|
|
98
|
+
"error": {
|
|
99
|
+
"color": "#F1F1F1",
|
|
100
|
+
"background_color": "#F05B5B"
|
|
101
|
+
},
|
|
102
|
+
"comment": {
|
|
103
|
+
"color": "#676767"
|
|
104
|
+
},
|
|
105
|
+
"comment_preproc": {
|
|
106
|
+
"color": "#FF875F"
|
|
107
|
+
},
|
|
108
|
+
"keyword": {
|
|
109
|
+
"color": "#00AAFF"
|
|
110
|
+
},
|
|
111
|
+
"keyword_reserved": {
|
|
112
|
+
"color": "#FF5FD2"
|
|
113
|
+
},
|
|
114
|
+
"keyword_namespace": {
|
|
115
|
+
"color": "#FF5F87"
|
|
116
|
+
},
|
|
117
|
+
"keyword_type": {
|
|
118
|
+
"color": "#6E6ED8"
|
|
119
|
+
},
|
|
120
|
+
"operator": {
|
|
121
|
+
"color": "#EF8080"
|
|
122
|
+
},
|
|
123
|
+
"punctuation": {
|
|
124
|
+
"color": "#E8E8A8"
|
|
125
|
+
},
|
|
126
|
+
"name": {
|
|
127
|
+
"color": "#C4C4C4"
|
|
128
|
+
},
|
|
129
|
+
"name_builtin": {
|
|
130
|
+
"color": "#FF8EC7"
|
|
131
|
+
},
|
|
132
|
+
"name_tag": {
|
|
133
|
+
"color": "#B083EA"
|
|
134
|
+
},
|
|
135
|
+
"name_attribute": {
|
|
136
|
+
"color": "#7A7AE6"
|
|
137
|
+
},
|
|
138
|
+
"name_class": {
|
|
139
|
+
"color": "#F1F1F1",
|
|
140
|
+
"underline": true,
|
|
141
|
+
"bold": true
|
|
142
|
+
},
|
|
143
|
+
"name_constant": {},
|
|
144
|
+
"name_decorator": {
|
|
145
|
+
"color": "#FFFF87"
|
|
146
|
+
},
|
|
147
|
+
"name_exception": {},
|
|
148
|
+
"name_function": {
|
|
149
|
+
"color": "#00D787"
|
|
150
|
+
},
|
|
151
|
+
"name_other": {},
|
|
152
|
+
"literal": {},
|
|
153
|
+
"literal_number": {
|
|
154
|
+
"color": "#6EEFC0"
|
|
155
|
+
},
|
|
156
|
+
"literal_date": {},
|
|
157
|
+
"literal_string": {
|
|
158
|
+
"color": "#C69669"
|
|
159
|
+
},
|
|
160
|
+
"literal_string_escape": {
|
|
161
|
+
"color": "#AFFFD7"
|
|
162
|
+
},
|
|
163
|
+
"generic_deleted": {
|
|
164
|
+
"color": "#FD5B5B"
|
|
165
|
+
},
|
|
166
|
+
"generic_emph": {
|
|
167
|
+
"italic": true
|
|
168
|
+
},
|
|
169
|
+
"generic_inserted": {
|
|
170
|
+
"color": "#00D787"
|
|
171
|
+
},
|
|
172
|
+
"generic_strong": {
|
|
173
|
+
"bold": true
|
|
174
|
+
},
|
|
175
|
+
"generic_subheading": {
|
|
176
|
+
"color": "#777777"
|
|
177
|
+
},
|
|
178
|
+
"background": {
|
|
179
|
+
"background_color": "#373737"
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
"table": {},
|
|
184
|
+
"definition_list": {},
|
|
185
|
+
"definition_term": {},
|
|
186
|
+
"definition_description": {
|
|
187
|
+
"block_prefix": "\n\ud83e\udc36 "
|
|
188
|
+
},
|
|
189
|
+
"html_block": {},
|
|
190
|
+
"html_span": {}
|
|
191
|
+
}
|